forked from rc/aircox
		
	work on lists filters + nav items
This commit is contained in:
		@ -185,12 +185,25 @@ class StaticPage(BasePage):
 | 
			
		||||
        (ATTACH_TO_EPISODES, _('Episodes list')),
 | 
			
		||||
        (ATTACH_TO_ARTICLES, _('Articles list')),
 | 
			
		||||
    )
 | 
			
		||||
    VIEWS = {
 | 
			
		||||
        ATTACH_TO_HOME: 'home',
 | 
			
		||||
        ATTACH_TO_DIFFUSIONS: 'diffusion-list',
 | 
			
		||||
        ATTACH_TO_LOGS: 'log-list',
 | 
			
		||||
        ATTACH_TO_PROGRAMS: 'program-list',
 | 
			
		||||
        ATTACH_TO_EPISODES: 'episode-list',
 | 
			
		||||
        ATTACH_TO_ARTICLES: 'article-list',
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    attach_to = models.SmallIntegerField(
 | 
			
		||||
        _('attach to'), choices=ATTACH_TO_CHOICES, blank=True, null=True,
 | 
			
		||||
        help_text=_('display this page content to related element'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        if self.attach_to:
 | 
			
		||||
            return reverse(self.VIEWS[self.attach_to])
 | 
			
		||||
        return super().get_absolute_url()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Comment(models.Model):
 | 
			
		||||
    page = models.ForeignKey(
 | 
			
		||||
@ -216,8 +229,7 @@ class NavItem(models.Model):
 | 
			
		||||
    text = models.CharField(_('title'), max_length=64)
 | 
			
		||||
    url = models.CharField(_('url'), max_length=256, blank=True, null=True)
 | 
			
		||||
    page = models.ForeignKey(StaticPage, models.CASCADE,
 | 
			
		||||
                             verbose_name=_('page'), blank=True, null=True,
 | 
			
		||||
                             limit_choices_to={'attach_to__isnull': True})
 | 
			
		||||
                             verbose_name=_('page'), blank=True, null=True)
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Menu item')
 | 
			
		||||
        verbose_name_plural = _('Menu items')
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -163,7 +163,7 @@
 | 
			
		||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
"use strict";
 | 
			
		||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"defaultConfig\", function() { return defaultConfig; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return AppBuilder; });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n\n\nconst defaultConfig = {\n    el: '#app',\n    delimiters: ['[[', ']]'],\n\n    computed: {\n        player() { return window.aircox.player; },\n    },\n}\n\n\nclass AppBuilder {\n    constructor(config={}) {\n        this._config = config;\n        this.app = null;\n    }\n\n    get config() {\n        let config = this._config instanceof Function ? this._config() : this._config;\n        for(var k of new Set([...Object.keys(config || {}), ...Object.keys(defaultConfig)])) {\n            if(!config[k] && defaultConfig[k])\n                config[k] = defaultConfig[k]\n            else if(config[k] instanceof Object)\n                config[k] = {...defaultConfig[k], ...config[k]}\n        }\n        return config;\n    }\n\n    set config(value) {\n        this._config = value;\n    }\n\n    destroy() {\n        self.app && self.app.$destroy();\n        self.app = null;\n    }\n\n    fetch(url, options) {\n        return fetch(url, options).then(response => response.text())\n            .then(content => {\n                let doc = new DOMParser().parseFromString(content, 'text/html');\n                let app = doc.getElementById('app');\n                content = app ? app.innerHTML : content;\n                return this.load({sync: true, content, title: doc.title, url })\n            })\n    }\n\n    load({async=false,content=null, title=null, url=null}={}) {\n        var self = this;\n        return new Promise((resolve, reject) => {\n            let func = () => {\n                try {\n                    let config = self.config;\n                    const el = document.querySelector(config.el);\n                    if(!el)\n                        return reject(`Error: can't get element ${config.el}`)\n\n                    if(content)\n                        el.innerHTML = content\n                    if(title)\n                        document.title = title;\n                    if(url && content)\n                        history.pushState({ content: content, title: title }, '', url)\n\n                    this.app = new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"](config);\n                    resolve(self.app)\n                } catch(error) {\n                    self.destroy();\n                    reject(error)\n                }};\n            async ? window.addEventListener('load', func) : func();\n        });\n    }\n\n    loadFromState(state) {\n        return this.load({ content: state.content, title: state.title });\n    }\n}\n\n\n\n\n//# sourceURL=webpack:///./assets/public/app.js?");
 | 
			
		||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"defaultConfig\", function() { return defaultConfig; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return AppBuilder; });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm.browser.js\");\n\n\nconst defaultConfig = {\n    el: '#app',\n    delimiters: ['[[', ']]'],\n\n    computed: {\n        player() { return window.aircox.player; },\n    },\n}\n\n\nclass AppBuilder {\n    constructor(config={}) {\n        this._config = config;\n        this.title = null;\n        this.app = null;\n    }\n\n    get config() {\n        let config = this._config instanceof Function ? this._config() : this._config;\n        for(var k of new Set([...Object.keys(config || {}), ...Object.keys(defaultConfig)])) {\n            if(!config[k] && defaultConfig[k])\n                config[k] = defaultConfig[k]\n            else if(config[k] instanceof Object)\n                config[k] = {...defaultConfig[k], ...config[k]}\n        }\n        return config;\n    }\n\n    set config(value) {\n        this._config = value;\n    }\n\n    destroy() {\n        self.app && self.app.$destroy();\n        self.app = null;\n    }\n\n    fetch(url, options) {\n        return fetch(url, options).then(response => response.text())\n            .then(content => {\n                let doc = new DOMParser().parseFromString(content, 'text/html');\n                let app = doc.getElementById('app');\n                content = app ? app.innerHTML : content;\n                return this.load({sync: true, content, title: doc.title, url })\n            })\n    }\n\n    load({async=false,content=null,title=null}={}) {\n        var self = this;\n        return new Promise((resolve, reject) => {\n            let func = () => {\n                try {\n                    let config = self.config;\n                    const el = document.querySelector(config.el);\n                    if(!el)\n                        return reject(`Error: can't get element ${config.el}`)\n\n                    if(content)\n                        el.innerHTML = content\n                    if(title)\n                        document.title = title;\n                    this.app = new vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"](config);\n                    resolve(self.app)\n                } catch(error) {\n                    self.destroy();\n                    reject(error)\n                }};\n            async ? window.addEventListener('load', func) : func();\n        });\n    }\n\n    /// Save application state into browser history\n    historySave(url,replace=false) {\n        const el = document.querySelector(this.config.el);\n        const state = {\n            content: el.innerHTML,\n            title: document.title,\n        };\n\n        if(replace)\n            history.replaceState(state, '', url)\n        else\n            history.pushState(state, '', url)\n    }\n\n    /// Load application from browser history's state\n    historyLoad(state) {\n        return this.load({ content: state.content, title: state.title });\n    }\n}\n\n\n\n\n//# sourceURL=webpack:///./assets/public/app.js?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ variables.
 | 
			
		||||
Usefull context:
 | 
			
		||||
- cover: image cover
 | 
			
		||||
- site: current website
 | 
			
		||||
- has_filters: display filter bar (using block "filters")
 | 
			
		||||
- model: view model or displayed `object`'s
 | 
			
		||||
- sidebar_object_list: item to display in sidebar
 | 
			
		||||
- sidebar_url_name: url name sidebar item complete list
 | 
			
		||||
@ -99,17 +98,6 @@ Usefull context:
 | 
			
		||||
                            <section class="content">{{ page.content|safe }}</section>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                            {% endblock %}
 | 
			
		||||
 | 
			
		||||
                            {# TODO: change block name #}
 | 
			
		||||
                            {% if has_filters %}
 | 
			
		||||
                            {% comment %}Translators: extra toolbar displayed on the top of page lists {% endcomment %}
 | 
			
		||||
                            <nav class="navbar toolbar"
 | 
			
		||||
                                aria-label="{% trans "list filters" %}">
 | 
			
		||||
                            {% block filters %}{% endblock %}
 | 
			
		||||
                            </nav>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                        {% endblock main %}
 | 
			
		||||
                    </main>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,49 @@
 | 
			
		||||
 | 
			
		||||
{% block main %}{{ block.super }}
 | 
			
		||||
 | 
			
		||||
{% block before_list %}
 | 
			
		||||
{% if filters %}
 | 
			
		||||
<form method="GET" action="" class="media">
 | 
			
		||||
    <div class="media-content">
 | 
			
		||||
        {% block filters %}
 | 
			
		||||
        {% for label, name, choices in filters %}
 | 
			
		||||
        <div class="field is-horizontal">
 | 
			
		||||
            <div class="field-label">
 | 
			
		||||
                <label class="label">{{ label }}</label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="field-body">
 | 
			
		||||
                <div class="field is-narrow">
 | 
			
		||||
                    <div class="control">
 | 
			
		||||
                        {% for label, value, checked in choices %}
 | 
			
		||||
                        <label class="checkbox">
 | 
			
		||||
                            <input type="checkbox" class="checkbox" name="{{ name }}"
 | 
			
		||||
                                   value="{{ value }}"
 | 
			
		||||
                                   {% if checked %}checked{% endif %} />
 | 
			
		||||
                            {{ label }}
 | 
			
		||||
                        </label>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="media-right">
 | 
			
		||||
        <div class="field is-grouped is-grouped-right">
 | 
			
		||||
            <div class="control">
 | 
			
		||||
                <button class="button is-primary"/>{% trans "Apply" %}</button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="control">
 | 
			
		||||
                <a href="?" class="button is-secondary">{% trans "Reset" %}</a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</form>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
<section role="list">
 | 
			
		||||
{% block pages_list %}
 | 
			
		||||
{% with has_headline=True %}
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
 | 
			
		||||
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block filters %}
 | 
			
		||||
{% block before_list %}
 | 
			
		||||
{% with "diffusion-list" as url_name %}
 | 
			
		||||
{% include "aircox/widgets/dates_menu.html" %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
 | 
			
		||||
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block filters %}
 | 
			
		||||
{% block before_list %}
 | 
			
		||||
{% with "log-list" as url_name %}
 | 
			
		||||
{% include "aircox/widgets/dates_menu.html" %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 | 
			
		||||
@ -2,49 +2,3 @@
 | 
			
		||||
{% comment %}Display a list of Pages{% endcomment %}
 | 
			
		||||
{% load i18n aircox %}
 | 
			
		||||
 | 
			
		||||
{% block filters %}
 | 
			
		||||
{# FIXME #}
 | 
			
		||||
{% if filter_categories %}
 | 
			
		||||
<form method="GET" action="" class="navbar-menu">
 | 
			
		||||
    <div class="navbar-start">
 | 
			
		||||
        <div class="navbar-item">
 | 
			
		||||
            {% block list_filters %}
 | 
			
		||||
            <div class="field is-horizontal">
 | 
			
		||||
                <div class="field-label">
 | 
			
		||||
                    <label class="label">{% trans "Categories" %}</label>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="field-body">
 | 
			
		||||
                    <div class="field is-narrow">
 | 
			
		||||
                        <div class="control">
 | 
			
		||||
                            {% for category in filter_categories %}
 | 
			
		||||
                            <label class="checkbox">
 | 
			
		||||
                                <input type="checkbox" class="checkbox" name="categories"
 | 
			
		||||
                                       value="{{ category.slug }}"
 | 
			
		||||
                                       {% if category.slug in categories %}checked{% endif %} />
 | 
			
		||||
                                {{ category.title }}
 | 
			
		||||
                            </label>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endblock %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="navbar-end">
 | 
			
		||||
        <div class="navbar-item">
 | 
			
		||||
            <div class="field is-grouped is-grouped-right">
 | 
			
		||||
                <div class="control">
 | 
			
		||||
                    <button class="button is-primary"/>{% trans "Apply" %}</button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="control">
 | 
			
		||||
                    <a href="?" class="button is-secondary">{% trans "Reset" %}</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</form>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
from collections import OrderedDict
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django.views.generic import ListView
 | 
			
		||||
 | 
			
		||||
from ..models import Diffusion, Episode, Program, StaticPage, Sound
 | 
			
		||||
@ -29,6 +30,17 @@ class EpisodeListView(PageListView):
 | 
			
		||||
    parent_model = Program
 | 
			
		||||
    attach_to_value = StaticPage.ATTACH_TO_EPISODES
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        qs = super().get_queryset()
 | 
			
		||||
        if self.filters and 'podcasts' in self.filters:
 | 
			
		||||
            qs = qs.filter(sound__is_public=True)
 | 
			
		||||
        return qs
 | 
			
		||||
 | 
			
		||||
    def get_filters(self):
 | 
			
		||||
        return super().get_filters() + (
 | 
			
		||||
            (_('Podcasts'), 'podcasts', tuple()),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiffusionListView(GetDateMixin, AttachedToMixin, BaseView, ListView):
 | 
			
		||||
    """ View for timetables """
 | 
			
		||||
 | 
			
		||||
@ -76,9 +76,11 @@ class PageListView(BasePageListView):
 | 
			
		||||
    template_name = None
 | 
			
		||||
    has_filters = True
 | 
			
		||||
    categories = None
 | 
			
		||||
    filters = None
 | 
			
		||||
 | 
			
		||||
    def get(self, *args, **kwargs):
 | 
			
		||||
        self.categories = set(self.request.GET.getlist('categories'))
 | 
			
		||||
        self.filters = set(self.request.GET.getlist('filters'))
 | 
			
		||||
        return super().get(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_template_names(self):
 | 
			
		||||
@ -94,16 +96,25 @@ class PageListView(BasePageListView):
 | 
			
		||||
            qs = qs.filter(category__slug__in=self.categories)
 | 
			
		||||
        return qs
 | 
			
		||||
 | 
			
		||||
    def get_categories_queryset(self):
 | 
			
		||||
        # TODO: use generic reverse field lookup
 | 
			
		||||
    def get_filters(self):
 | 
			
		||||
        categories = self.model.objects.published() \
 | 
			
		||||
                               .filter(category__isnull=False) \
 | 
			
		||||
                               .values_list('category', flat=True)
 | 
			
		||||
        return Category.objects.filter(id__in=categories)
 | 
			
		||||
        categories = [ (c.title, c.slug, c.slug in self.categories)
 | 
			
		||||
                        for c in Category.objects.filter(id__in=categories) ]
 | 
			
		||||
        return (
 | 
			
		||||
            (_('Categories'), 'categories', categories),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        kwargs.setdefault('filter_categories', self.get_categories_queryset())
 | 
			
		||||
        kwargs.setdefault('categories', self.categories)
 | 
			
		||||
        if not 'filters' in kwargs:
 | 
			
		||||
            filters = self.get_filters()
 | 
			
		||||
            for label, fieldName, choices in filters:
 | 
			
		||||
                if choices:
 | 
			
		||||
                    kwargs['filters'] = filters
 | 
			
		||||
                    break;
 | 
			
		||||
            else:
 | 
			
		||||
                kwargs['filters'] = tuple()
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ export const defaultConfig = {
 | 
			
		||||
export default class AppBuilder {
 | 
			
		||||
    constructor(config={}) {
 | 
			
		||||
        this._config = config;
 | 
			
		||||
        this.title = null;
 | 
			
		||||
        this.app = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -46,7 +47,7 @@ export default class AppBuilder {
 | 
			
		||||
            })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    load({async=false,content=null, title=null, url=null}={}) {
 | 
			
		||||
    load({async=false,content=null,title=null}={}) {
 | 
			
		||||
        var self = this;
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
            let func = () => {
 | 
			
		||||
@ -60,9 +61,6 @@ export default class AppBuilder {
 | 
			
		||||
                        el.innerHTML = content
 | 
			
		||||
                    if(title)
 | 
			
		||||
                        document.title = title;
 | 
			
		||||
                    if(url && content)
 | 
			
		||||
                        history.pushState({ content: content, title: title }, '', url)
 | 
			
		||||
 | 
			
		||||
                    this.app = new Vue(config);
 | 
			
		||||
                    resolve(self.app)
 | 
			
		||||
                } catch(error) {
 | 
			
		||||
@ -73,7 +71,22 @@ export default class AppBuilder {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    loadFromState(state) {
 | 
			
		||||
    /// Save application state into browser history
 | 
			
		||||
    historySave(url,replace=false) {
 | 
			
		||||
        const el = document.querySelector(this.config.el);
 | 
			
		||||
        const state = {
 | 
			
		||||
            content: el.innerHTML,
 | 
			
		||||
            title: document.title,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if(replace)
 | 
			
		||||
            history.replaceState(state, '', url)
 | 
			
		||||
        else
 | 
			
		||||
            history.pushState(state, '', url)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Load application from browser history's state
 | 
			
		||||
    historyLoad(state) {
 | 
			
		||||
        return this.load({ content: state.content, title: state.title });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ window.aircox = {
 | 
			
		||||
    get playerApp() { return this.playerBuilder && this.playerBuilder.app },
 | 
			
		||||
    get player() { return this.playerApp && this.playerApp.$refs.player },
 | 
			
		||||
 | 
			
		||||
    // Handle hot-reload (link click and form submits).
 | 
			
		||||
    onPageFetch(event) {
 | 
			
		||||
        let submit = event.type == 'submit';
 | 
			
		||||
        let target = submit || event.target.tagName == 'A'
 | 
			
		||||
@ -62,7 +63,9 @@ window.aircox = {
 | 
			
		||||
                options['body'] = formData;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        this.appBuilder.fetch(url, options);
 | 
			
		||||
        this.appBuilder.fetch(url, options).then(app => {
 | 
			
		||||
            this.appBuilder.historySave(url);
 | 
			
		||||
        });
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
    },
 | 
			
		||||
@ -76,12 +79,16 @@ aircox.playerBuilder = new AppBuilder({el: '#player'});
 | 
			
		||||
aircox.playerBuilder.load({async:true});
 | 
			
		||||
aircox.appBuilder = new AppBuilder(x => window.aircox.appConfig);
 | 
			
		||||
aircox.appBuilder.load({async:true}).then(app => {
 | 
			
		||||
    aircox.appBuilder.historySave(document.location, true);
 | 
			
		||||
 | 
			
		||||
    //-- load page hooks
 | 
			
		||||
    window.addEventListener('click', event => aircox.onPageFetch(event), true);
 | 
			
		||||
    window.addEventListener('submit', event => aircox.onPageFetch(event), true);
 | 
			
		||||
    window.addEventListener('popstate', event => {
 | 
			
		||||
        if(event.state && event.state.content)
 | 
			
		||||
            aircox.appBuilder.loadFromState(event.state);
 | 
			
		||||
        if(event.state && event.state.content) {
 | 
			
		||||
            document.title = aircox.appBuilder.title;
 | 
			
		||||
            aircox.appBuilder.historyLoad(event.state);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user