Merge branch 'master' into gender_options

This commit is contained in:
Celeste
2021-10-11 17:13:06 +02:00
64 changed files with 1740 additions and 1161 deletions

View File

@ -0,0 +1,107 @@
import re
from subprocess import PIPE, Popen, TimeoutExpired
from django.conf import settings
from django.core.management.base import BaseCommand
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
# added "v?"
semver_regex = re.compile(
"""^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"""
)
class Command(BaseCommand):
help = "Checks the front dependencies are up to date."
def handle(self, *args, **options):
deps = settings.SITH_FRONT_DEP_VERSIONS
processes = dict(
(url, create_process(url))
for url in deps.keys()
if parse_semver(deps[url]) is not None
)
for url, process in processes.items():
try:
stdout, stderr = process.communicate(timeout=15)
except TimeoutExpired:
process.kill()
self.stderr.write(self.style.WARNING("{}: timeout".format(url)))
continue
# error, notice, warning
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
if stderr != "":
self.stderr.write(self.style.WARNING(stderr.strip()))
continue
# get all tags, parse them as semvers and find the biggest
tags = list_tags(stdout)
tags = map(parse_semver, tags)
tags = filter(lambda tag: tag is not None, tags)
latest_version = max(tags)
# cannot fail as those which fail are filtered in the processes dict creation
current_version = parse_semver(deps[url])
assert current_version is not None
if latest_version == current_version:
msg = "{}: {}".format(url, semver_to_s(current_version))
self.stdout.write(self.style.SUCCESS(msg))
else:
msg = "{}: {} < {}".format(
url, semver_to_s(current_version), semver_to_s(latest_version)
)
self.stdout.write(self.style.ERROR(msg))
def create_process(url):
"""Spawn a "git ls-remote --tags" child process."""
return Popen(["git", "ls-remote", "--tags", url], stdout=PIPE, stderr=PIPE)
def list_tags(s):
"""Parses "git ls-remote --tags" output. Takes a string."""
tag_prefix = "refs/tags/"
for line in s.strip().split("\n"):
# an example line could be:
# "1f41e2293f9c3c1962d2d97afa666207b98a222a\trefs/tags/foo"
parts = line.split("\t")
# check we have a commit ID (SHA-1 hash) and a tag name
assert len(parts) == 2
assert len(parts[0]) == 40
assert parts[1].startswith(tag_prefix)
# avoid duplicates (a peeled tag will appear twice: as "name" and as "name^{}")
if not parts[1].endswith("^{}"):
yield parts[1][len(tag_prefix) :]
def parse_semver(s):
"""
Turns a semver string into a 3-tuple or None if the parsing failed, it is a
prerelease or it has build metadata.
See https://semver.org
"""
m = semver_regex.match(s)
if (
m is None
or m.group("prerelease") is not None
or m.group("buildmetadata") is not None
):
return None
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
def semver_to_s(t):
"""Expects a 3-tuple with ints and turns it into a string of type "1.2.3"."""
return "{}.{}.{}".format(t[0], t[1], t[2])

View File

@ -31,7 +31,7 @@ from django.conf import settings
class Command(BaseCommand):
"""
Compiles scss in static folder for production
Compiles scss in static folder for production
"""
help = "Compile scss files from static folder"

View File

@ -1492,7 +1492,9 @@ class OperationLog(models.Model):
User, related_name="logs", on_delete=models.SET_NULL, null=True
)
operation_type = models.CharField(
_("operation type"), max_length=40, choices=settings.SITH_LOG_OPERATION_TYPE,
_("operation type"),
max_length=40,
choices=settings.SITH_LOG_OPERATION_TYPE,
)
def is_owned_by(self, user):

View File

@ -33,16 +33,16 @@ from django.db import connection, migrations
class PsqlRunOnly(migrations.RunSQL):
"""
This is an SQL runner that will launch the given command only if
the used DBMS is PostgreSQL.
It may be useful to run Postgres' specific SQL, or to take actions
that would be non-senses with backends other than Postgre, such
as disabling particular constraints that would prevent the migration
to run successfully.
This is an SQL runner that will launch the given command only if
the used DBMS is PostgreSQL.
It may be useful to run Postgres' specific SQL, or to take actions
that would be non-senses with backends other than Postgre, such
as disabling particular constraints that would prevent the migration
to run successfully.
See `club/migrations/0010_auto_20170912_2028.py` as an example.
Some explanations can be found here too:
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
See `club/migrations/0010_auto_20170912_2028.py` as an example.
Some explanations can be found here too:
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
"""
def _run_sql(self, schema_editor, sqls):

View File

@ -35,9 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
class ScssProcessor(object):
"""
If DEBUG mode enabled : compile the scss file
Else : give the path of the corresponding css supposed to already be compiled
Don't forget to use compilestatics to compile scss for production
If DEBUG mode enabled : compile the scss file
Else : give the path of the corresponding css supposed to already be compiled
Don't forget to use compilestatics to compile scss for production
"""
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))

View File

@ -34,6 +34,7 @@ from forum.models import ForumMessage, ForumMessageMeta
class UserIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
auto = indexes.EdgeNgramField(use_template=True)
last_update = indexes.DateTimeField(model_attr="last_update")
def get_model(self):
return User
@ -45,6 +46,9 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
def get_updated_field(self):
return "last_update"
def prepare_auto(self, obj):
return self.prepared_data["auto"].strip()[:245]
class IndexSignalProcessor(signals.BaseSignalProcessor):
def setup(self):

File diff suppressed because one or more lines are too long

View File

@ -28,7 +28,7 @@ $twitblue: hsl(206, 82%, 63%);
$shadow-color: rgb(223, 223, 223);
$background-bouton-color: hsl(0, 0%, 90%);
$background-button-color: hsl(0, 0%, 95%);
/*--------------------------MEDIA QUERY HELPERS------------------------*/
$small-devices: 576px;
@ -47,10 +47,11 @@ body {
input[type=button], input[type=submit], input[type=reset],input[type=file] {
border: none;
text-decoration: none;
background-color: $background-bouton-color;
padding: 10px;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-weight: bold;
font-size: 16px;
font-size: 1.2em;
border-radius: 5px;
cursor: pointer;
box-shadow: $shadow-color 0px 0px 1px;
@ -62,9 +63,10 @@ input[type=button], input[type=submit], input[type=reset],input[type=file] {
button{
border: none;
text-decoration: none;
background-color: $background-bouton-color;
padding: 10px;
font-size: 14px;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-size: 1.18em;
border-radius: 5px;
box-shadow: $shadow-color 0px 0px 1px;
cursor: pointer;
@ -75,24 +77,26 @@ button{
input,textarea[type=text],[type=number]{
border: none;
text-decoration: none;
background-color: $background-bouton-color;
padding: 7px;
font-size: 16px;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-size: 1.2em;
border-radius: 5px;
max-width: 95%;
}
textarea{
border: none;
text-decoration: none;
background-color: $background-bouton-color;
background-color: $background-button-color;
padding: 7px;
font-size: 16px;
font-size: 1.2em;
border-radius: 5px;
}
select{
border: none;
text-decoration: none;
font-size: 15px;
background-color: $background-bouton-color;
font-size: 1.2em;
background-color: $background-button-color;
padding: 10px;
border-radius: 5px;
cursor: pointer;
@ -130,9 +134,10 @@ a {
#header_language_chooser {
position: absolute;
top: 0.2em;
right: 0.5em;
top: 2em;
left: 0.5em;
width: 3%;
min-width: 2.2em;
text-align: center;
input {
display: block;
@ -157,9 +162,6 @@ header {
border-radius: 0px 0px 10px 10px;
#header_logo {
display: inline-block;
flex: none;
background-size: 100% 100%;
background-color: $white-color;
padding: 0.2em;
border-radius: 0px 0px 0px 9px;
@ -169,11 +171,19 @@ header {
margin: 0px;
width: 100%;
height: 100%;
img {
max-width: 70%;
max-height: 100%;
margin: auto;
display: block;
}
}
}
#header_connect_links {
margin: 0.6em 0.6em 0em auto;
padding: 0.2em;
color: $white-color;
form {
display: inline;
@ -190,8 +200,9 @@ header {
#header_bar {
display: flex;
flex: auto;
flex-wrap: wrap;
width: 80%;
a {
text-decoration: none;
margin: 0em 1em;
@ -203,7 +214,6 @@ header {
}
#header_bars_infos {
width: 35ch;
flex: initial;
list-style-type: none;
margin: 0.2em 0.2em;
@ -213,12 +223,15 @@ header {
display: inline-block;
flex: auto;
margin: 0.8em 0em;
input {
width: 14ch;
}
}
#header_user_links {
display: flex;
width: 120ch;
flex: initial;
flex-wrap: wrap;
text-align: right;
margin: 0em;
div {
@ -287,42 +300,34 @@ header {
#info_boxes {
display: flex;
flex-wrap: wrap;
width: 90%;
margin: 1em auto;
p {
margin: 0px;
padding: 7px;
}
#alert_box, #info_box {
font-size: 14px;
display: inline-block;
flex: auto;
padding: 2px;
margin: 0.2em 1.5%;
min-width: 10%;
max-width: 46%;
min-height: 20px;
flex: 49%;
font-size: 0.9em;
margin: 0.2em;
border-radius: 0.6em;
.markdown {
margin: 0.5em;
}
&:before {
float: left;
font-family: FontAwesome;
font-size: 4em;
float: right;
margin: 0.2em;
}
}
#info_box {
border-radius: 10px;
background: $primary-neutral-light-color;
&:before {
font-family: FontAwesome;
font-size: 4em;
content: "\f05a";
color: hsl(210, 100%, 56%);
}
}
#alert_box {
border-radius: 10px;
background: $second-color;
&:before {
font-family: FontAwesome;
font-size: 4em;
content: "\f06a";
color: $white-color;
}
@ -345,12 +350,12 @@ header {
a {
flex: auto;
text-align: center;
padding: 20px;
padding: 1.5em;
color: $white-color;
font-style: normal;
font-weight: bolder;
text-decoration: none;
&:hover {
background: $secondary-neutral-color;
color: $white-color;
@ -458,6 +463,8 @@ header {
/*---------------------------------NEWS--------------------------------*/
#news {
display: flex;
flex-wrap: wrap;
.news_column {
display: inline-block;
margin: 0px;
@ -467,11 +474,13 @@ header {
margin-bottom: 1em;
}
#right_column {
width: 20%;
flex: 20%;
float: right;
margin: 0.2em;
}
#left_column {
width: 79%;
flex: 79%;
margin: 0.2em;
h3 {
background: $second-color;
box-shadow: $shadow-color 1px 1px 1px;
@ -484,6 +493,11 @@ header {
}
}
}
@media screen and (max-width: $small-devices){
#left_column, #right_column {
flex: 100%;
}
}
/* AGENDA/BIRTHDAYS */
#agenda,#birthdays {
@ -691,6 +705,12 @@ header {
}
}
@media screen and (max-width: $small-devices){
#page {
width: 98%;
}
}
#news_details {
display: inline-block;
margin-top: 20px;
@ -723,7 +743,7 @@ header {
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
font-size: 1.2em;
border-radius: 2px;
float: right;
display: block;
@ -1111,33 +1131,36 @@ u, .underline {
text-decoration: underline;
}
#basket {
width: 40%;
background: $primary-neutral-light-color;
float: right;
padding: 10px;
border-radius: 10px;
}
#products {
width: 90%;
margin: 0px auto;
overflow: auto;
}
#bar_ui {
float: left;
min-width: 57%;
}
padding: 0.4em;
display: flex;
flex-wrap: wrap;
flex-direction: row-reverse;
#user_info_container {}
#products {
flex-basis: 100%;
margin: 0.2em;
overflow: auto;
}
#user_info {
float: right;
padding: 5px;
width: 40%;
margin: 0px auto;
background: $secondary-neutral-light-color;
#click_form {
flex: auto;
margin: 0.2em;
}
#user_info {
flex: auto;
padding: 0.5em;
margin: 0.2em;
height: 100%;
background: $secondary-neutral-light-color;
img {
max-width: 70%;
}
input {
background: white;
}
}
}
/*-----------------------------USER PROFILE----------------------------*/
@ -1212,6 +1235,11 @@ u, .underline {
}
}
}
@media screen and (max-width: $small-devices){
#user_profile_infos, #user_profile_pictures {
flex-basis: 50%;
}
}
}
}
@ -1412,6 +1440,7 @@ textarea {
.search_bar {
margin: 10px 0px;
display: flex;
flex-wrap: wrap;
height: 20p;
align-items: center;
}
@ -1551,6 +1580,7 @@ footer {
color: $white-color;
border-radius: 5px;
display: flex;
flex-wrap: wrap;
background-color: $primary-neutral-dark-color;
box-shadow: $shadow-color 0px 0px 15px;
a {
@ -2181,4 +2211,4 @@ $pedagogy-white-text: #f0f0f0;
}
}
}
}
}

View File

@ -3,6 +3,7 @@
<head>
{% block head %}
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link rel="stylesheet" href="{{ static('core/base.css') }}">
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
@ -27,6 +28,7 @@
<!-- BEGIN HEADER -->
{% block header %}
{% if not popup %}
<header>
<div id="header_language_chooser">
{% for language in LANGUAGES %}
<form action="{{ url('set_language') }}" method="post">{% csrf_token %}
@ -37,10 +39,11 @@
{% endfor %}
</div>
<header>
{% if not user.is_authenticated %}
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 185px; height: 100px;">
<a href="{{ url('core:index') }}"></a>
<div id="header_logo">
<a href="{{ url('core:index') }}">
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
</a>
</div>
<div id="header_connect_links">
<form method="post" action="{{ url('core:login') }}">
@ -54,12 +57,14 @@
<a href="{{ url('core:register') }}"><button type="button">{% trans %}Register{% endtrans %}</button></a>
</div>
{% else %}
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 92px; height: 52px;">
<a href="{{ url('core:index') }}"></a>
<div id="header_logo">
<a href="{{ url('core:index') }}">
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
</a>
</div>
<div id="header_bar">
<ul id="header_bars_infos">
{% cache 100 counters_activity %}
{% cache 100 "counters_activity" %}
{% for bar in Counter.objects.filter(type="BAR").all() %}
<li>
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px">
@ -85,7 +90,7 @@
<a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a>
</div>
<div>
<a href="#" onclick="display_notif()"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a>
<a href="#" onclick="display_notif()" style="white-space: nowrap;"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a>
<ul id="header_notif">
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}
<li>
@ -126,17 +131,19 @@
</header>
<div id="info_boxes">
{% set sith = get_sith() %}
{% if sith.alert_msg %}
<div id="alert_box">
{{ sith.alert_msg|markdown }}
</div>
{% endif %}
{% if sith.info_msg %}
<div id="info_box">
{{ sith.info_msg|markdown }}
</div>
{% endif %}
{% block info_boxes %}
{% set sith = get_sith() %}
{% if sith.alert_msg %}
<div id="alert_box">
{{ sith.alert_msg|markdown }}
</div>
{% endif %}
{% if sith.info_msg %}
<div id="info_box">
{{ sith.info_msg|markdown }}
</div>
{% endif %}
{% endblock %}
</div>
{% else %}{# if not popup #}

View File

@ -4,6 +4,12 @@
{% trans %}Delete confirmation{% endtrans %}
{% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
<form action="" method="post">{% csrf_token %}

View File

@ -52,6 +52,7 @@
{% if not form.instance.profile_pict %}
<script src="{{ static('core/js/webcam.js') }}"></script>
<script language="JavaScript">
Webcam.on('error', function(msg) { console.log('Webcam.js error: ' + msg) })
Webcam.set({
width: 320,
height: 240,

View File

@ -12,7 +12,7 @@
{% for picture in pictures[a.id] %}
<div class="picture">
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
<img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%"/>
<img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%" loading="lazy"/>
</a>
</div>
{% endfor %}

View File

@ -1,3 +1,13 @@
{{ object.first_name }}
{{ object.last_name }}
{{ object.nick_name }}
{% load search_helpers %}
{% with first=object.first_name|safe|slugify last=object.last_name|safe|slugify nick=object.nick_name|default_if_none:""|safe|slugify %}
{{ first|replace:"|-| " }}
{{ last|replace:"|-| " }}
{{ nick|replace:"|-| " }}
{% if first|count:"-" != 0 %}{{ first|cut:"-" }}{% endif %}
{% if last|count:"-" != 0 %}{{ last|cut:"-" }}{% endif %}
{% if nick|count:"-" != 0 %}{{ nick|cut:"-" }}{% endif %}
{{ first|cut:"-" }}{{ last|cut:"-" }}
{% endwith %}

View File

@ -94,7 +94,7 @@ def datetime_format_python_to_PHP(python_format_string):
@register.simple_tag()
def scss(path):
"""
Return path of the corresponding css file after compilation
Return path of the corresponding css file after compilation
"""
processor = ScssProcessor(path)
return processor.get_converted_scss()

View File

@ -0,0 +1,27 @@
from django.template.exceptions import TemplateSyntaxError
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
# arg should be of the form "|foo|bar" where the first character is the
# separator between old and new in value.replace(old, new)
@register.filter
@stringfilter
def replace(value, arg):
# s.replace('', '') == s so len(arg) == 2 is fine
if len(arg) < 2:
raise TemplateSyntaxError("badly formatted argument")
arg = arg.split(arg[0])
if len(arg) != 3:
raise TemplateSyntaxError("badly formatted argument")
return value.replace(arg[1], arg[2])
@register.filter
def count(value, arg):
return value.count(arg)

View File

@ -344,19 +344,19 @@ class QuickNotifMixin:
class DetailFormView(SingleObjectMixin, FormView):
"""
Class that allow both a detail view and a form view
Class that allow both a detail view and a form view
"""
def get_object(self):
"""
Get current group from id in url
Get current group from id in url
"""
return self.cached_object
@cached_property
def cached_object(self):
"""
Optimisation on group retrieval
Optimisation on group retrieval
"""
return super(DetailFormView, self).get_object()

View File

@ -42,6 +42,10 @@ from django.utils.translation import ugettext
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
from ajax_select.fields import AutoCompleteSelectField
from ajax_select import make_ajax_field
from django.utils.dateparse import parse_datetime
from django.utils import timezone
import datetime
from django.forms.utils import to_current_timezone
import re
@ -114,14 +118,11 @@ class SelectFile(TextInput):
attrs["class"] = "select_file"
else:
attrs = {"class": "select_file"}
output = (
'%(content)s<div name="%(name)s" class="choose_file_widget" title="%(title)s"></div>'
% {
"content": super(SelectFile, self).render(name, value, attrs, renderer),
"title": _("Choose file"),
"name": name,
}
)
output = '%(content)s<div name="%(name)s" class="choose_file_widget" title="%(title)s"></div>' % {
"content": super(SelectFile, self).render(name, value, attrs, renderer),
"title": _("Choose file"),
"name": name,
}
output += (
'<span name="'
+ name
@ -138,14 +139,11 @@ class SelectUser(TextInput):
attrs["class"] = "select_user"
else:
attrs = {"class": "select_user"}
output = (
'%(content)s<div name="%(name)s" class="choose_user_widget" title="%(title)s"></div>'
% {
"content": super(SelectUser, self).render(name, value, attrs, renderer),
"title": _("Choose user"),
"name": name,
}
)
output = '%(content)s<div name="%(name)s" class="choose_user_widget" title="%(title)s"></div>' % {
"content": super(SelectUser, self).render(name, value, attrs, renderer),
"title": _("Choose user"),
"name": name,
}
output += (
'<span name="'
+ name
@ -399,3 +397,26 @@ class GiftForm(forms.ModelForm):
id=user_id
)
self.fields["user"].widget = forms.HiddenInput()
class TzAwareDateTimeField(forms.DateTimeField):
def __init__(
self, input_formats=["%Y-%m-%d %H:%M:%S"], widget=SelectDateTime, **kwargs
):
super().__init__(input_formats=input_formats, widget=widget, **kwargs)
def prepare_value(self, value):
# the db value is a datetime as a string in UTC
if isinstance(value, str):
# convert it into a naive datetime (no timezone attached)
value = parse_datetime(value)
# attach it to the UTC timezone (so that to_current_timezone()
# converts it to the local timezone)
value = timezone.make_aware(value, timezone.utc)
if isinstance(value, datetime.datetime):
value = to_current_timezone(value)
# otherwise it is formatted according to locale (in french)
value = str(value)
return value

View File

@ -44,7 +44,7 @@ from core.views import CanEditMixin, DetailFormView
class EditMembersForm(forms.Form):
"""
Add and remove members from a Group
Add and remove members from a Group
"""
def __init__(self, *args, **kwargs):
@ -66,7 +66,7 @@ class EditMembersForm(forms.Form):
def clean_users_added(self):
"""
Check that the user is not trying to add an user already in the group
Check that the user is not trying to add an user already in the group
"""
cleaned_data = super(EditMembersForm, self).clean()
users_added = cleaned_data.get("users_added", None)
@ -100,7 +100,7 @@ class GroupListView(CanEditMixin, ListView):
class GroupEditView(CanEditMixin, UpdateView):
"""
Edit infos of a Group
Edit infos of a Group
"""
model = RealGroup
@ -111,7 +111,7 @@ class GroupEditView(CanEditMixin, UpdateView):
class GroupCreateView(CanEditMixin, CreateView):
"""
Add a new Group
Add a new Group
"""
model = RealGroup
@ -121,8 +121,8 @@ class GroupCreateView(CanEditMixin, CreateView):
class GroupTemplateView(CanEditMixin, DetailFormView):
"""
Display all users in a given Group
Allow adding and removing users from it
Display all users in a given Group
Allow adding and removing users from it
"""
model = RealGroup
@ -156,7 +156,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
class GroupDeleteView(CanEditMixin, DeleteView):
"""
Delete a Group
Delete a Group
"""
model = RealGroup

View File

@ -30,6 +30,7 @@ from django.contrib.auth.decorators import login_required
from django.utils import html
from django.views.generic import ListView, TemplateView
from django.conf import settings
from django.utils.text import slugify
import json
@ -73,7 +74,18 @@ def notification(request, notif_id):
def search_user(query, as_json=False):
try:
res = SearchQuerySet().models(User).autocomplete(auto=html.escape(query))[:20]
# slugify turns everything into ascii and every whitespace into -
# it ends by removing duplicate - (so ' - ' will turn into '-')
# replace('-', ' ') because search is whitespace based
query = slugify(query).replace("-", " ")
# TODO: is this necessary?
query = html.escape(query)
res = (
SearchQuerySet()
.models(User)
.autocomplete(auto=query)
.order_by("-last_update")[:20]
)
return [r.object for r in res]
except TypeError:
return []