add home layout customization
This commit is contained in:
parent
c220cbd9b3
commit
5268fc5aa2
10 changed files with 410 additions and 11 deletions
|
@ -808,6 +808,20 @@ select::placeholder {
|
|||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.icon-edit svg {
|
||||
fill: #ccc;
|
||||
height: 12px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.icon-save svg {
|
||||
fill: #ccc;
|
||||
height: 12px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.icon-cross svg {
|
||||
fill: #ccc;
|
||||
height: 10px;
|
||||
|
@ -1529,6 +1543,7 @@ select::placeholder {
|
|||
}
|
||||
|
||||
.entity-sort {
|
||||
position: relative;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
@ -1582,6 +1597,58 @@ select::placeholder {
|
|||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.entity-sort--placeholder {
|
||||
border: dashed #bbb 4px;
|
||||
}
|
||||
|
||||
.entity-sort--hover {
|
||||
padding: 10px;
|
||||
border: dashed #00a1cc 2px !important;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entity-sort--sortable {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: dashed #bbb 2px;
|
||||
cursor: all-scroll;
|
||||
}
|
||||
|
||||
.entity-sort--hidden {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.entity-sort-control {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.entity-sort-control__button {
|
||||
margin-top: 5px;
|
||||
margin-left: 12px;
|
||||
padding: 0 2px;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.entity-sort-control__button:hover {
|
||||
color: #00a1cc;
|
||||
}
|
||||
|
||||
.entity-sort-control__button:hover > .icon-save svg, .entity-sort-control__button:hover > .icon-edit svg {
|
||||
fill: #00a1cc;
|
||||
}
|
||||
|
||||
.entity-sort-control__button--float-right {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.related-user-list .related-user-list__title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
|
2
common/static/css/boofilsic.min.css
vendored
2
common/static/css/boofilsic.min.css
vendored
File diff suppressed because one or more lines are too long
143
common/static/js/sort_layout.js
Normal file
143
common/static/js/sort_layout.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
$(() => {
|
||||
|
||||
// initialization
|
||||
|
||||
// add toggle display button
|
||||
$(".entity-sort").each((i, e) => {
|
||||
if ($(e).data("visibility") === undefined) {
|
||||
$(e).data("visibility", true);
|
||||
}
|
||||
let btn = $("#toggleDisplayButtonTemplate").clone().removeAttr("id");
|
||||
btn.click(e => {
|
||||
if ($(e.currentTarget).parent().data('visibility') === true) {
|
||||
// flip text
|
||||
$(e.currentTarget).children("span.showText").show();
|
||||
$(e.currentTarget).children("span.hideText").hide();
|
||||
// flip data
|
||||
$(e.currentTarget).parent().data('visibility', false);
|
||||
// flip display
|
||||
$(e.currentTarget).parent().addClass("entity-sort--hidden");
|
||||
} else {
|
||||
// flip text
|
||||
$(e.currentTarget).children("span.showText").hide();
|
||||
$(e.currentTarget).children("span.hideText").show();
|
||||
// flip data
|
||||
$(e.currentTarget).parent().data('visibility', true);
|
||||
// flip display
|
||||
$(e.currentTarget).parent().removeClass("entity-sort--hidden");
|
||||
}
|
||||
});
|
||||
$(e).prepend(btn);
|
||||
});
|
||||
// initialize toggle buttons
|
||||
initialLayoutData.forEach(elem => {
|
||||
if (elem.visibility) {
|
||||
$('#' + elem.id).find("span.showText").hide();
|
||||
$('#' + elem.id).find("span.hideText").show();
|
||||
} else {
|
||||
$('#' + elem.id).find("span.showText").show();
|
||||
$('#' + elem.id).find("span.hideText").hide();
|
||||
}
|
||||
});
|
||||
// initialize the sortable plugin
|
||||
sortable('.sortable', {
|
||||
forcePlaceholderSize: true,
|
||||
placeholderClass: 'entity-sort--placeholder',
|
||||
// hoverClass: 'entity-sort--hover'
|
||||
});
|
||||
sortable('.sortable', 'disable');
|
||||
// set state flag
|
||||
let isActivated = false;
|
||||
// set class modifier, because the effect of the plugin is not very well
|
||||
let dragging = false;
|
||||
$(".entity-sort").on('mouseenter', (e) => {
|
||||
if (isActivated && !dragging) {
|
||||
$(e.currentTarget).addClass("entity-sort--hover");
|
||||
}
|
||||
});
|
||||
$(".entity-sort").on('mouseleave', (e) => {
|
||||
if (isActivated) {
|
||||
$(e.currentTarget).removeClass("entity-sort--hover");
|
||||
}
|
||||
});
|
||||
$(".entity-sort").on('dragstart', (e) => {
|
||||
if (isActivated) {
|
||||
dragging = true;
|
||||
}
|
||||
});
|
||||
$(".entity-sort").on('dragend', (e) => {
|
||||
if (isActivated) {
|
||||
dragging = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// activate sorting
|
||||
$("#sortEditButton").click(e => {
|
||||
// test if edit mode is activated
|
||||
isActivated = $("#sortSaveIcon").is(":visible");
|
||||
|
||||
if (isActivated) {
|
||||
// save edited layout
|
||||
|
||||
let rawData = sortable('.sortable', 'serialize')[0].items;
|
||||
let serializedData = []
|
||||
|
||||
// collect layout information
|
||||
for (const key in rawData) {
|
||||
if (Object.hasOwnProperty.call(rawData, key)) {
|
||||
const sort = rawData[key];
|
||||
let id = $(sort.node).attr("id");
|
||||
let visibility = $(sort.node).data("visibility") ? true : false;
|
||||
serializedData.push({
|
||||
id: id,
|
||||
visibility: visibility
|
||||
});
|
||||
}
|
||||
}
|
||||
$("#sortForm input[name='layout']").val(JSON.stringify(serializedData))
|
||||
$("#sortForm").submit();
|
||||
// console.log(serializedData)
|
||||
|
||||
|
||||
} else {
|
||||
// enter edit mode
|
||||
$("#sortSaveIcon").show();
|
||||
$("#sortEditIcon").hide();
|
||||
$("#sortSaveText").show();
|
||||
$("#sortEditText").hide();
|
||||
$("#sortExitButton").show();
|
||||
sortable('.sortable', 'enable');
|
||||
$(".entity-sort").each((index, elem) => {
|
||||
$(elem).show();
|
||||
$(elem).addClass("entity-sort--sortable");
|
||||
if ($(elem).data('visibility') === true) {
|
||||
$(elem).find("span.showText").hide();
|
||||
$(elem).find("span.hideText").show();
|
||||
} else if ($(elem).data('visibility') === false) {
|
||||
$(elem).find("span.showText").show();
|
||||
$(elem).find("span.hideText").hide();
|
||||
}
|
||||
$(elem).children(".entity-sort-control__button").show();
|
||||
if ($(elem).data('visibility') === false) {
|
||||
$(elem).addClass("entity-sort--hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
isActivated = $("#sortSaveIcon").is(":visible");
|
||||
});
|
||||
|
||||
// exit edit mode
|
||||
$("#sortExitButton").click(e => {
|
||||
$("#sortSaveIcon").hide();
|
||||
$("#sortEditIcon").show();
|
||||
$("#sortSaveText").hide();
|
||||
$("#sortEditText").show();
|
||||
$("#sortExitButton").hide();
|
||||
sortable('.sortable', 'disable');
|
||||
$(".entity-sort").each((index, elem) => {
|
||||
$(elem).removeClass("entity-sort--sortable");
|
||||
});
|
||||
isActivated = $("#sortSaveIcon").is(":visible");
|
||||
});
|
||||
});
|
|
@ -5,6 +5,17 @@
|
|||
top: 1px
|
||||
margin-left: 3px
|
||||
|
||||
.icon-edit svg
|
||||
fill: $color-light
|
||||
height: 12px
|
||||
position: relative
|
||||
top: 2px
|
||||
|
||||
.icon-save svg
|
||||
fill: $color-light
|
||||
height: 12px
|
||||
position: relative
|
||||
top: 2px
|
||||
|
||||
.icon-cross svg
|
||||
fill: $color-light
|
||||
|
|
|
@ -262,6 +262,7 @@ $mark-review-padding-wider: 6px 0
|
|||
|
||||
// on home page
|
||||
.entity-sort
|
||||
position: relative
|
||||
margin-bottom: 30px
|
||||
& &__label
|
||||
font-size: large
|
||||
|
@ -299,6 +300,45 @@ $mark-review-padding-wider: 6px 0
|
|||
-webkit-line-clamp: 2
|
||||
|
||||
& &__empty
|
||||
|
||||
// for drag and sort purpose
|
||||
&--placeholder
|
||||
border: dashed $color-tertiary 4px
|
||||
&--hover
|
||||
padding: 10px
|
||||
border: dashed $color-primary 2px !important
|
||||
border-radius: 3px
|
||||
&--sortable
|
||||
padding: 10px
|
||||
margin: 10px 0
|
||||
border: dashed $color-tertiary 2px
|
||||
cursor: all-scroll
|
||||
&--hidden
|
||||
opacity: 0.4
|
||||
|
||||
|
||||
.entity-sort-control
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
|
||||
&__button
|
||||
margin-top: 5px
|
||||
margin-left: 12px
|
||||
padding: 0 2px
|
||||
cursor: pointer
|
||||
color: $color-tertiary
|
||||
&:hover
|
||||
color: $color-primary
|
||||
& > .icon-save svg, & > .icon-edit svg
|
||||
fill: $color-primary
|
||||
|
||||
&--float-right
|
||||
position: absolute
|
||||
top: 4px
|
||||
right: 10px
|
||||
|
||||
&__text
|
||||
|
||||
|
||||
// follower/following list page
|
||||
.related-user-list
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<div class="grid grid--reverse-order">
|
||||
<div class="grid__main grid__main--reverse-order">
|
||||
|
||||
<div class="main-section-wrapper">
|
||||
<div class="main-section-wrapper sortable">
|
||||
|
||||
<div class="entity-sort" id="bookWish">
|
||||
<h5 class="entity-sort__label">
|
||||
|
@ -43,7 +43,6 @@
|
|||
<a href="{% url 'users:book_list' user.id 'wish' %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for wish_book_mark in wish_book_marks %}
|
||||
<li class="entity-sort__entity">
|
||||
|
@ -300,6 +299,73 @@
|
|||
|
||||
|
||||
</div>
|
||||
|
||||
{% if user == request.user %}
|
||||
|
||||
<div class="entity-sort-control">
|
||||
<div class="entity-sort-control__button" id="sortEditButton">
|
||||
<span class="entity-sort-control__text" id="sortEditText">
|
||||
{% trans '编辑布局' %}
|
||||
</span>
|
||||
<span class="entity-sort-control__text" id="sortSaveText" style="display: none;">
|
||||
{% trans '保存' %}
|
||||
</span>
|
||||
<span class="icon-edit" id="sortEditIcon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 383.947 383.947">
|
||||
<polygon points="0,303.947 0,383.947 80,383.947 316.053,147.893 236.053,67.893 " />
|
||||
<path
|
||||
d="M377.707,56.053L327.893,6.24c-8.32-8.32-21.867-8.32-30.187,0l-39.04,39.04l80,80l39.04-39.04 C386.027,77.92,386.027,64.373,377.707,56.053z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="icon-save" id="sortSaveIcon" style="display: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 384 384" >
|
||||
<path
|
||||
d="M298.667,0h-256C19.093,0,0,19.093,0,42.667v298.667C0,364.907,19.093,384,42.667,384h298.667 C364.907,384,384,364.907,384,341.333v-256L298.667,0z M192,341.333c-35.307,0-64-28.693-64-64c0-35.307,28.693-64,64-64 s64,28.693,64,64C256,312.64,227.307,341.333,192,341.333z M256,128H42.667V42.667H256V128z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="entity-sort-control__button" id="sortExitButton" style="display: none;">
|
||||
<span class="entity-sort-control__text">
|
||||
{% trans '取消' %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entity-sort-control__button entity-sort-control__button--float-right" id="toggleDisplayButtonTemplate" style="display: none;">
|
||||
<span class="showText" style="display: none;">
|
||||
{% trans '显示' %}
|
||||
</span>
|
||||
<span class="hideText" style="display: none;">
|
||||
{% trans '隐藏' %}
|
||||
</span>
|
||||
</div>
|
||||
<form action="{% url 'users:set_layout' %}" method="post" id="sortForm">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="layout">
|
||||
</form>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.10.0/html5sortable.min.js"
|
||||
integrity="sha512-tBlVMq89XaEC9iU5LyRjP2Vxs8SmVhEHGbv2Co6SbGa14Wsxy2qZN0jadrN+Xn5AifORaUbvZcG21/ExcNfWDA=="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/sort_layout.js' %}"></script>
|
||||
{% endif %}
|
||||
<script>
|
||||
const initialLayoutData = JSON.parse("{{ layout|escapejs }}");
|
||||
// initialize sort element visibility and order
|
||||
initialLayoutData.forEach(elem => {
|
||||
// False to false, True to true
|
||||
if (elem.visibility === "False") {
|
||||
elem.visibility = false;
|
||||
} else {
|
||||
elem.visibility = true;
|
||||
}
|
||||
// set visiblity
|
||||
$('#' + elem.id).data('visibility', elem.visibility);
|
||||
if (!elem.visibility) {
|
||||
$('#' + elem.id).hide();
|
||||
}
|
||||
// order
|
||||
$('#' + elem.id).appendTo('.main-section-wrapper');
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -492,6 +558,7 @@
|
|||
$(this).parents(".modal").hide();
|
||||
$(".bg-mask").hide();
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ from django.http import HttpResponseBadRequest
|
|||
from books.models import Book
|
||||
from movies.models import Movie
|
||||
from music.models import Album, Song, AlbumMark, SongMark
|
||||
from users.models import Report, User
|
||||
from users.models import Report, User, Preference
|
||||
from mastodon.decorators import mastodon_request_included
|
||||
from common.models import MarkStatusEnum
|
||||
from common.utils import PageLinksGenerator
|
||||
|
@ -105,6 +105,12 @@ def home(request):
|
|||
reports = Report.objects.order_by('-submitted_time').filter(is_read=False)
|
||||
# reports = Report.objects.latest('submitted_time').filter(is_read=False)
|
||||
|
||||
try:
|
||||
layout = request.user.preference.get_serialized_home_layout()
|
||||
except ObjectDoesNotExist:
|
||||
Preference.objects.create(user=request.user)
|
||||
layout = request.user.preference.get_serialized_home_layout()
|
||||
|
||||
return render(
|
||||
request,
|
||||
'common/home.html',
|
||||
|
@ -129,6 +135,7 @@ def home(request):
|
|||
'collect_music_more': collect_music_more,
|
||||
'reports': reports,
|
||||
'unread_announcements': unread_announcements,
|
||||
'layout': layout,
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import uuid
|
||||
import django.contrib.postgres.fields as postgres
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.utils import timezone
|
||||
from boofilsic.settings import REPORT_MEDIA_PATH_ROOT, DEFAULT_PASSWORD
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
def report_image_path(instance, filename):
|
||||
|
@ -39,6 +42,18 @@ class User(AbstractUser):
|
|||
return self.username + '@' + self.mastodon_site
|
||||
|
||||
|
||||
class Preference(models.Model):
|
||||
user = models.OneToOneField(User, models.CASCADE, primary_key=True)
|
||||
home_layout = postgres.ArrayField(
|
||||
postgres.HStoreField(),
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
|
||||
def get_serialized_home_layout(self):
|
||||
return str(self.home_layout).replace("\'","\"")
|
||||
|
||||
|
||||
class Report(models.Model):
|
||||
submit_user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='sumbitted_reports', null=True)
|
||||
reported_user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='accused_reports', null=True)
|
||||
|
@ -46,6 +61,3 @@ class Report(models.Model):
|
|||
is_read = models.BooleanField(default=False)
|
||||
submitted_time = models.DateTimeField(auto_now_add=True)
|
||||
message = models.CharField(max_length=1000)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ urlpatterns = [
|
|||
path('register/', register, name='register'),
|
||||
path('logout/', logout, name='logout'),
|
||||
path('delete/', delete, name='delete'),
|
||||
path('layout/', set_layout, name='set_layout'),
|
||||
path('OAuth2_login/', OAuth2_login, name='OAuth2_login'),
|
||||
path('<int:id>/', home, name='home'),
|
||||
path('<int:id>/followers/', followers, name='followers'),
|
||||
|
|
|
@ -7,12 +7,12 @@ from django.core.paginator import Paginator
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Count
|
||||
from .models import User, Report
|
||||
from .models import User, Report, Preference
|
||||
from .forms import ReportForm
|
||||
from mastodon.auth import *
|
||||
from mastodon.api import *
|
||||
from mastodon import mastodon_request_included
|
||||
from common.views import BOOKS_PER_SET, ITEMS_PER_PAGE, PAGE_LINK_NUMBER, TAG_NUMBER_ON_LIST, MOVIES_PER_SET
|
||||
from common.views import BOOKS_PER_SET, ITEMS_PER_PAGE, PAGE_LINK_NUMBER, TAG_NUMBER_ON_LIST, MOVIES_PER_SET, MUSIC_PER_SET
|
||||
from common.models import MarkStatusEnum
|
||||
from common.utils import PageLinksGenerator
|
||||
from books.models import *
|
||||
|
@ -195,11 +195,42 @@ def home(request, id):
|
|||
wish_movies_more = True if wish_movie_marks.count() > BOOKS_PER_SET else False
|
||||
|
||||
collect_movie_marks = movie_marks.filter(status=MarkStatusEnum.COLLECT)
|
||||
collect_movies_more = True if collect_movie_marks.count() > BOOKS_PER_SET else False
|
||||
collect_movies_more = True if collect_movie_marks.count() > BOOKS_PER_SET else False
|
||||
|
||||
song_marks = SongMark.get_available_by_user(user, relation['following'])
|
||||
album_marks = AlbumMark.get_available_by_user(user, relation['following'])
|
||||
|
||||
do_music_marks = list(song_marks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) \
|
||||
+ list(album_marks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET])
|
||||
do_music_more = True if len(do_music_marks) > MUSIC_PER_SET else False
|
||||
do_music_marks = sorted(do_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET]
|
||||
|
||||
wish_music_marks = list(song_marks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) \
|
||||
+ list(album_marks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET])
|
||||
wish_music_more = True if len(wish_music_marks) > MUSIC_PER_SET else False
|
||||
wish_music_marks = sorted(wish_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET]
|
||||
|
||||
collect_music_marks = list(song_marks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) \
|
||||
+ list(album_marks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET])
|
||||
collect_music_more = True if len(collect_music_marks) > MUSIC_PER_SET else False
|
||||
collect_music_marks = sorted(collect_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET]
|
||||
|
||||
for mark in do_music_marks + wish_music_marks + collect_music_marks:
|
||||
# for template convenience
|
||||
if mark.__class__ == AlbumMark:
|
||||
mark.type = "album"
|
||||
else:
|
||||
mark.type = "song"
|
||||
|
||||
user.target_site_id = get_cross_site_id(
|
||||
user, request.user.mastodon_site, request.session['oauth_token'])
|
||||
|
||||
try:
|
||||
layout = user.preference.get_serialized_home_layout()
|
||||
except ObjectDoesNotExist:
|
||||
Preference.objects.create(user=request.user)
|
||||
layout = user.preference.get_serialized_home_layout()
|
||||
|
||||
return render(
|
||||
request,
|
||||
'common/home.html',
|
||||
|
@ -217,6 +248,13 @@ def home(request, id):
|
|||
'do_movies_more': do_movies_more,
|
||||
'wish_movies_more': wish_movies_more,
|
||||
'collect_movies_more': collect_movies_more,
|
||||
'do_music_marks': do_music_marks,
|
||||
'wish_music_marks': wish_music_marks,
|
||||
'collect_music_marks': collect_music_marks,
|
||||
'do_music_more': do_music_more,
|
||||
'wish_music_more': wish_music_more,
|
||||
'collect_music_more': collect_music_more,
|
||||
'layout': layout,
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
@ -543,7 +581,20 @@ def music_list(request, id, status):
|
|||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def set_layout(request):
|
||||
if request.method == 'POST':
|
||||
# json to python
|
||||
raw_layout_data = request.POST.get('layout').replace('false', 'False').replace('true', 'True')
|
||||
layout = eval(raw_layout_data)
|
||||
request.user.preference.home_layout = eval(raw_layout_data)
|
||||
request.user.preference.save()
|
||||
return redirect(reverse("common:home"))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def report(request):
|
||||
|
|
Loading…
Add table
Reference in a new issue