mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-07-18 15:20:19 +02:00
Manage page (no js yet)
This commit is contained in:
@ -8,7 +8,7 @@ from note_kfet.inputs import Autocomplete
|
|||||||
from .models import Challenge, FamilyMembership, User, Family
|
from .models import Challenge, FamilyMembership, User, Family
|
||||||
|
|
||||||
|
|
||||||
class ChallengeUpdateForm(forms.ModelForm):
|
class ChallengeForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
To update a challenge
|
To update a challenge
|
||||||
"""
|
"""
|
||||||
@ -20,6 +20,12 @@ class ChallengeUpdateForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Family
|
||||||
|
fields = ('name', 'description', )
|
||||||
|
|
||||||
|
|
||||||
class FamilyMembershipForm(forms.ModelForm):
|
class FamilyMembershipForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FamilyMembership
|
model = FamilyMembership
|
||||||
@ -36,9 +42,3 @@ class FamilyMembershipForm(forms.ModelForm):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FamilyUpdateForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Family
|
|
||||||
fields = ('description', )
|
|
263
apps/family/static/family/js/achievements.js
Normal file
263
apps/family/static/family/js/achievements.js
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
// Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
// When a transaction is performed, lock the interface to prevent spam clicks.
|
||||||
|
var LOCK = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the history table on the consumptions page.
|
||||||
|
*/
|
||||||
|
function refreshHistory () {
|
||||||
|
$('#history').load('/note/consos/ #history')
|
||||||
|
$('#most_used').load('/note/consos/ #most_used')
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
// If hash of a category in the URL, then select this category
|
||||||
|
// else select the first one
|
||||||
|
if (location.hash) {
|
||||||
|
$("a[href='" + location.hash + "']").tab('show')
|
||||||
|
} else {
|
||||||
|
$("a[data-toggle='tab']").first().tab('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
// When selecting a category, change URL
|
||||||
|
$(document.body).on('click', "a[data-toggle='tab']", function () {
|
||||||
|
location.hash = this.getAttribute('href')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById("consume_all").addEventListener('click', consumeAll)
|
||||||
|
})
|
||||||
|
|
||||||
|
notes = []
|
||||||
|
notes_display = []
|
||||||
|
buttons = []
|
||||||
|
|
||||||
|
// When the user searches an alias, we update the auto-completion
|
||||||
|
autoCompleteNote('note', 'note_list', notes, notes_display,
|
||||||
|
'alias', 'note', 'user_note', 'profile_pic', function () {
|
||||||
|
if (buttons.length > 0 && $('#single_conso').is(':checked')) {
|
||||||
|
consumeAll()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a transaction from a button.
|
||||||
|
* @param dest Where the money goes
|
||||||
|
* @param amount The price of the item
|
||||||
|
* @param type The type of the transaction (content type id for RecurrentTransaction)
|
||||||
|
* @param category_id The category identifier
|
||||||
|
* @param category_name The category name
|
||||||
|
* @param template_id The identifier of the button
|
||||||
|
* @param template_name The name of the button
|
||||||
|
*/
|
||||||
|
function addConso (dest, amount, type, category_id, category_name, template_id, template_name) {
|
||||||
|
var button = null
|
||||||
|
buttons.forEach(function (b) {
|
||||||
|
if (b.id === template_id) {
|
||||||
|
b.quantity += 1
|
||||||
|
button = b
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (button == null) {
|
||||||
|
button = {
|
||||||
|
id: template_id,
|
||||||
|
name: template_name,
|
||||||
|
dest: dest,
|
||||||
|
quantity: 1,
|
||||||
|
amount: amount,
|
||||||
|
type: type,
|
||||||
|
category_id: category_id,
|
||||||
|
category_name: category_name
|
||||||
|
}
|
||||||
|
buttons.push(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dc_obj = $('#double_conso')
|
||||||
|
if (dc_obj.is(':checked') || notes_display.length === 0) {
|
||||||
|
const list = dc_obj.is(':checked') ? 'consos_list' : 'note_list'
|
||||||
|
let html = ''
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
html += li('conso_button_' + button.id, button.name +
|
||||||
|
'<span class="badge badge-dark badge-pill">' + button.quantity + '</span>')
|
||||||
|
})
|
||||||
|
document.getElementById(list).innerHTML = html
|
||||||
|
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
|
||||||
|
if (LOCK) { return }
|
||||||
|
removeNote(button, 'conso_button', buttons, list)()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else { consumeAll() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the page as its initial state.
|
||||||
|
*/
|
||||||
|
function reset () {
|
||||||
|
notes_display.length = 0
|
||||||
|
notes.length = 0
|
||||||
|
buttons.length = 0
|
||||||
|
document.getElementById('note_list').innerHTML = ''
|
||||||
|
document.getElementById('consos_list').innerHTML = ''
|
||||||
|
document.getElementById('note').value = ''
|
||||||
|
document.getElementById('note').dataset.originTitle = ''
|
||||||
|
$('#note').tooltip('hide')
|
||||||
|
document.getElementById('profile_pic').src = '/static/member/img/default_picture.png'
|
||||||
|
document.getElementById('profile_pic_link').href = '#'
|
||||||
|
refreshHistory()
|
||||||
|
refreshBalance()
|
||||||
|
LOCK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply all transactions: all notes in `notes` buy each item in `buttons`
|
||||||
|
*/
|
||||||
|
function consumeAll () {
|
||||||
|
if (LOCK) { return }
|
||||||
|
|
||||||
|
LOCK = true
|
||||||
|
|
||||||
|
let error = false
|
||||||
|
|
||||||
|
if (notes_display.length === 0) {
|
||||||
|
document.getElementById('note').classList.add('is-invalid')
|
||||||
|
$('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger'))
|
||||||
|
error = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttons.length === 0) {
|
||||||
|
$('#consos_list').html(li('', '<strong>Ajoutez des consommations.</strong>', 'text-danger'))
|
||||||
|
error = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
LOCK = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notes_display.forEach(function (note_display) {
|
||||||
|
buttons.forEach(function (button) {
|
||||||
|
consume(note_display.note, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount,
|
||||||
|
button.name + ' (' + button.category_name + ')', button.type, button.category_id, button.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new transaction from a button through the API.
|
||||||
|
* @param source The note that paid the item (type: note)
|
||||||
|
* @param source_alias The alias used for the source (type: str)
|
||||||
|
* @param dest The note that sold the item (type: int)
|
||||||
|
* @param quantity The quantity sold (type: int)
|
||||||
|
* @param amount The price of one item, in cents (type: int)
|
||||||
|
* @param reason The transaction details (type: str)
|
||||||
|
* @param type The type of the transaction (content type id for RecurrentTransaction)
|
||||||
|
* @param category The category id of the button (type: int)
|
||||||
|
* @param template The button id (type: int)
|
||||||
|
*/
|
||||||
|
function consume (source, source_alias, dest, quantity, amount, reason, type, category, template) {
|
||||||
|
$.post('/api/note/transaction/transaction/',
|
||||||
|
{
|
||||||
|
csrfmiddlewaretoken: CSRF_TOKEN,
|
||||||
|
quantity: quantity,
|
||||||
|
amount: amount,
|
||||||
|
reason: reason,
|
||||||
|
valid: true,
|
||||||
|
polymorphic_ctype: type,
|
||||||
|
resourcetype: 'RecurrentTransaction',
|
||||||
|
source: source.id,
|
||||||
|
source_alias: source_alias,
|
||||||
|
destination: dest,
|
||||||
|
template: template
|
||||||
|
})
|
||||||
|
.done(function () {
|
||||||
|
if (!isNaN(source.balance)) {
|
||||||
|
const newBalance = source.balance - quantity * amount
|
||||||
|
if (newBalance <= -2000) {
|
||||||
|
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
||||||
|
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
|
||||||
|
} else if (newBalance < 0) {
|
||||||
|
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
||||||
|
'but the emitter note %s is negative.'), [source_alias, source_alias]), 'warning', 30000)
|
||||||
|
}
|
||||||
|
if (source.membership && source.membership.date_end < new Date().toISOString()) {
|
||||||
|
addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source_alias]),
|
||||||
|
'danger', 30000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reset()
|
||||||
|
}).fail(function (e) {
|
||||||
|
$.post('/api/note/transaction/transaction/',
|
||||||
|
{
|
||||||
|
csrfmiddlewaretoken: CSRF_TOKEN,
|
||||||
|
quantity: quantity,
|
||||||
|
amount: amount,
|
||||||
|
reason: reason,
|
||||||
|
valid: false,
|
||||||
|
invalidity_reason: 'Solde insuffisant',
|
||||||
|
polymorphic_ctype: type,
|
||||||
|
resourcetype: 'RecurrentTransaction',
|
||||||
|
source: source.id,
|
||||||
|
source_alias: source_alias,
|
||||||
|
destination: dest,
|
||||||
|
template: template
|
||||||
|
}).done(function () {
|
||||||
|
reset()
|
||||||
|
addMsg(gettext("The transaction couldn't be validated because of insufficient balance."), 'danger', 10000)
|
||||||
|
}).fail(function () {
|
||||||
|
reset()
|
||||||
|
errMsg(e.responseJSON)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchbar = document.getElementById("search-input")
|
||||||
|
var search_results = document.getElementById("search-results")
|
||||||
|
|
||||||
|
var old_pattern = null;
|
||||||
|
var firstMatch = null;
|
||||||
|
/**
|
||||||
|
* Updates the button search tab
|
||||||
|
* @param force Forces the update even if the pattern didn't change
|
||||||
|
*/
|
||||||
|
function updateSearch(force = false) {
|
||||||
|
let pattern = searchbar.value
|
||||||
|
if (pattern === "")
|
||||||
|
firstMatch = null;
|
||||||
|
if ((pattern === old_pattern || pattern === "") && !force)
|
||||||
|
return;
|
||||||
|
firstMatch = null;
|
||||||
|
const re = new RegExp(pattern, "i");
|
||||||
|
Array.from(search_results.children).forEach(function(b) {
|
||||||
|
if (re.test(b.innerText)) {
|
||||||
|
b.hidden = false;
|
||||||
|
if (firstMatch === null) {
|
||||||
|
firstMatch = b;
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
b.hidden = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchbar.addEventListener("input", function (e) {
|
||||||
|
debounce(updateSearch)()
|
||||||
|
});
|
||||||
|
searchbar.addEventListener("keyup", function (e) {
|
||||||
|
if (firstMatch && e.key === "Enter")
|
||||||
|
firstMatch.click()
|
||||||
|
});
|
||||||
|
|
||||||
|
function createshiny() {
|
||||||
|
const list_btn = document.querySelectorAll('.btn-outline-dark')
|
||||||
|
const shiny_class = list_btn[Math.floor(Math.random() * list_btn.length)].classList
|
||||||
|
shiny_class.replace('btn-outline-dark', 'btn-outline-dark-shiny')
|
||||||
|
}
|
||||||
|
createshiny()
|
@ -2,10 +2,9 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2 import A
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .models import Family, Challenge, FamilyMembership
|
from .models import Family, Challenge, FamilyMembership, Achievement
|
||||||
|
|
||||||
|
|
||||||
class FamilyTable(tables.Table):
|
class FamilyTable(tables.Table):
|
||||||
@ -58,3 +57,17 @@ class FamilyMembershipTable(tables.Table):
|
|||||||
template_name = 'django_tables2/bootstrap4.html'
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
fields = ('user',)
|
fields = ('user',)
|
||||||
model = FamilyMembership
|
model = FamilyMembership
|
||||||
|
|
||||||
|
|
||||||
|
class AchievementTable(tables.Table):
|
||||||
|
"""
|
||||||
|
List recent achievements.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table-condensed table-striped table-hover'
|
||||||
|
}
|
||||||
|
model = Achievement
|
||||||
|
fields = ('family', 'challenge', 'obtained_at', )
|
||||||
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
orderable = False
|
||||||
|
@ -16,11 +16,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<a href="#" class="btn btn-sm btn-outline-primary active">
|
<a href="#" class="btn btn-sm btn-outline-primary active">
|
||||||
{% trans "Challenges" %}
|
{% trans "Challenges" %}
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url "family:manage" %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
{% trans "Manage" %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<<div class="card bg-white mb-3">
|
<div class="card bg-white mb-3">
|
||||||
<h3 class="card-header text-center">
|
<h3 class="card-header text-center">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -16,11 +16,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<a href="{% url "family:challenge_list" %}" class="btn btn-sm btn-outline-primary">
|
<a href="{% url "family:challenge_list" %}" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Challenges" %}
|
{% trans "Challenges" %}
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url "family:manage" %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
{% trans "Manage" %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<<div class="card bg-white mb-3">
|
<div class="card bg-white mb-3">
|
||||||
<h3 class="card-header text-center">
|
<h3 class="card-header text-center">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
|
205
apps/family/templates/family/manage.html
Normal file
205
apps/family/templates/family/manage.html
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% comment %}
|
||||||
|
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
{% endcomment %}
|
||||||
|
{% load i18n static django_tables2 %}
|
||||||
|
|
||||||
|
{% block containertype %}container-fluid{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-12">
|
||||||
|
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0">
|
||||||
|
<a href="{% url "family:family_list" %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
{% trans "Families" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url "family:challenge_list" %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
{% trans "Challenges" %}
|
||||||
|
</a>
|
||||||
|
<a href="#" class="btn btn-sm btn-outline-primary active">
|
||||||
|
{% trans "Manage" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class='col-sm-5 col-xl-6' id="infos_div">
|
||||||
|
<div class="row justify-content-center justify-content-md-end">
|
||||||
|
{# User details column #}
|
||||||
|
<div class="col picture-col">
|
||||||
|
<div class="card bg-light mb-4 text-center">
|
||||||
|
<a id="profile_pic_link" href="#">
|
||||||
|
<img src="{% static "member/img/default_picture.png" %}"
|
||||||
|
id="profile_pic" alt="" class="card-img-top d-none d-sm-block">
|
||||||
|
</a>
|
||||||
|
<div class="card-body text-center text-break p-2">
|
||||||
|
<span id="user_note"><i class="small">{% trans "Please select a family" %}</i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Family selection column #}
|
||||||
|
<div class="col-xl" id="user_select_div">
|
||||||
|
<div class="card bg-light border-success mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<p class="card-text font-weight-bold">
|
||||||
|
{% trans "Families" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0" style="min-height:125px;">
|
||||||
|
<ul class="list-group list-group-flush" id="note_list">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# User search with autocompletion #}
|
||||||
|
<div class="card-footer">
|
||||||
|
<input class="form-control mx-auto d-block"
|
||||||
|
placeholder="{% trans "Name" %}" type="text" id="note" autofocus />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Summary of challenges and validate button #}
|
||||||
|
<div class="col-xl-5" id="consos_list_div">
|
||||||
|
<div class="card bg-light border-info mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<p class="card-text font-weight-bold">
|
||||||
|
{% trans "Challenges" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0" style="min-height:125px;">
|
||||||
|
<ul class="list-group list-group-flush" id="consos_list">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<span id="consume_all" class="btn btn-primary">
|
||||||
|
{% trans "Validate!" %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{# Create family/challenge buttons #}
|
||||||
|
<div class="card bg-light border-success mb-4">
|
||||||
|
<h3 class="card-header">
|
||||||
|
<p class="card-text font-weight-bold">
|
||||||
|
{% trans "Create a family or challenge" %}
|
||||||
|
</p>
|
||||||
|
</h3>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if can_add_family %}
|
||||||
|
<a class="btn btn-sm btn-primary" href="{% url "family:add_family" %}">
|
||||||
|
{% trans "Add a family" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_add_challenge %}
|
||||||
|
<a class="btn btn-sm btn-primary" href="{% url "family:add_challenge" %}">
|
||||||
|
{% trans "Add a challenge" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{# Buttons column #}
|
||||||
|
<div class="col">
|
||||||
|
{# Regroup buttons under categories #}
|
||||||
|
|
||||||
|
<div class="card bg-light border-primary text-center mb-4">
|
||||||
|
{# Tabs for list and search #}
|
||||||
|
<div class="card-header">
|
||||||
|
<ul class="nav nav-tabs nav-fill card-header-tabs">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link font-weight-bold" data-toggle="tab" href="#list">
|
||||||
|
{% trans "List" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link font-weight-bold" data-toggle="tab" href="#search">
|
||||||
|
{% trans "Search" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Tabs content #}
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane" id="list">
|
||||||
|
<div class="d-inline-flex flex-wrap justify-content-center">
|
||||||
|
{% for challenge in all_challenges %}
|
||||||
|
<button class="btn btn-outline-dark rounded-0 flex-fill"
|
||||||
|
id="challenge{{ challenge.id }}" name="button" value="{{ challenge.name }}">
|
||||||
|
{{ challenge.name }} ({{ challenge.points }} {% trans "points" %})
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane" id="search">
|
||||||
|
<input class="form-control mx-auto d-block mb-3"
|
||||||
|
placeholder="{% trans "Search challenge..." %}" type="search" id="search-input"/>
|
||||||
|
<div class="d-inline-flex flex-wrap justify-content-center" id="search-results">
|
||||||
|
{% for challenge in all_challenges %}
|
||||||
|
<button class="btn btn-outline-dark rounded-0 flex-fill" hidden
|
||||||
|
id="search_challenge{{ challenge.id }}" name="button" value="{{ challenge.name }}">
|
||||||
|
{{ challenge.name }} ({{ challenge.points }} {% trans "points" %})
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Mode switch #}
|
||||||
|
<div class="card-footer border-primary">
|
||||||
|
<a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}">
|
||||||
|
<i class="fa fa-edit"></i> {% trans "Edit" %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{# transaction history #}
|
||||||
|
<div class="card mb-4" id="history">
|
||||||
|
<div class="card-header">
|
||||||
|
<p class="card-text font-weight-bold">
|
||||||
|
{% trans "Recent achievements history" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% render_table table %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script type="text/javascript" src="{% static "family/js/consos.js" %}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
{% for button in all_challenges %}
|
||||||
|
document.getElementById("button{{ button.id }}").addEventListener("click", function() {
|
||||||
|
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||||
|
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||||
|
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||||
|
});
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for button in all_challenges %}
|
||||||
|
{% if button.display %}
|
||||||
|
document.getElementById("search_button{{ button.id }}").addEventListener("click", function() {
|
||||||
|
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||||
|
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||||
|
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -3,16 +3,19 @@
|
|||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import FamilyListView, FamilyDetailView, FamilyUpdateView, FamilyPictureUpdateView, FamilyAddMemberView, ChallengeListView, ChallengeDetailView, ChallengeUpdateView
|
from . import views
|
||||||
|
|
||||||
app_name = 'family'
|
app_name = 'family'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('list/', FamilyListView.as_view(), name="family_list"),
|
path('list/', views.FamilyListView.as_view(), name="family_list"),
|
||||||
path('detail/<int:pk>/', FamilyDetailView.as_view(), name="family_detail"),
|
path('add-family/', views.FamilyCreateView.as_view(), name="add_family"),
|
||||||
path('update/<int:pk>/', FamilyUpdateView.as_view(), name="family_update"),
|
path('detail/<int:pk>/', views.FamilyDetailView.as_view(), name="family_detail"),
|
||||||
path('update_pic/<int:pk>/', FamilyPictureUpdateView.as_view(), name="update_pic"),
|
path('update/<int:pk>/', views.FamilyUpdateView.as_view(), name="family_update"),
|
||||||
path('add_member/<int:family_pk>/', FamilyAddMemberView.as_view(), name="family_add_member"),
|
path('update_pic/<int:pk>/', views.FamilyPictureUpdateView.as_view(), name="update_pic"),
|
||||||
path('challenge/list/', ChallengeListView.as_view(), name="challenge_list"),
|
path('add_member/<int:family_pk>/', views.FamilyAddMemberView.as_view(), name="family_add_member"),
|
||||||
path('challenge/detail/<int:pk>/', ChallengeDetailView.as_view(), name="challenge_detail"),
|
path('challenge/list/', views.ChallengeListView.as_view(), name="challenge_list"),
|
||||||
path('challenge/update/<int:pk>/', ChallengeUpdateView.as_view(), name="challenge_update"),
|
path('add-challenge/', views.ChallengeCreateView.as_view(), name="add_challenge"),
|
||||||
|
path('challenge/detail/<int:pk>/', views.ChallengeDetailView.as_view(), name="challenge_detail"),
|
||||||
|
path('challenge/update/<int:pk>/', views.ChallengeUpdateView.as_view(), name="challenge_update"),
|
||||||
|
path('manage/', views.FamilyManageView.as_view(), name="manage"),
|
||||||
]
|
]
|
||||||
|
@ -12,13 +12,12 @@ from django_tables2 import SingleTableView
|
|||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
from .models import Family, Challenge, FamilyMembership, User
|
|
||||||
from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable
|
|
||||||
from .forms import ChallengeUpdateForm, FamilyMembershipForm, FamilyUpdateForm
|
|
||||||
from member.forms import ImageForm
|
|
||||||
from member.views import PictureUpdateView
|
from member.views import PictureUpdateView
|
||||||
|
|
||||||
|
from .models import Family, Challenge, FamilyMembership, User, Achievement
|
||||||
|
from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable, AchievementTable
|
||||||
|
from .forms import ChallengeForm, FamilyMembershipForm, FamilyForm
|
||||||
|
|
||||||
|
|
||||||
class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
"""
|
"""
|
||||||
@ -26,6 +25,7 @@ class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
"""
|
"""
|
||||||
model = Family
|
model = Family
|
||||||
extra_context = {"title": _('Create family')}
|
extra_context = {"title": _('Create family')}
|
||||||
|
form_class = FamilyForm
|
||||||
|
|
||||||
def get_sample_object(self):
|
def get_sample_object(self):
|
||||||
return Family(
|
return Family(
|
||||||
@ -35,6 +35,10 @@ class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
rank=0,
|
rank=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
self.object.refresh_from_db()
|
||||||
|
return reverse_lazy("family:manage")
|
||||||
|
|
||||||
|
|
||||||
class FamilyListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
class FamilyListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
"""
|
"""
|
||||||
@ -92,8 +96,7 @@ class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||||||
"""
|
"""
|
||||||
model = Family
|
model = Family
|
||||||
context_object_name = "family"
|
context_object_name = "family"
|
||||||
form_class = FamilyUpdateForm
|
form_class = FamilyForm
|
||||||
template_name = 'family/family_update.html'
|
|
||||||
extra_context = {"title": _('Update family')}
|
extra_context = {"title": _('Update family')}
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@ -152,7 +155,6 @@ class FamilyAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
form = context['form']
|
|
||||||
|
|
||||||
family = Family.objects.filter(PermissionBackend.filter_queryset(self.request, Family, "view"))\
|
family = Family.objects.filter(PermissionBackend.filter_queryset(self.request, Family, "view"))\
|
||||||
.get(pk=self.kwargs['family_pk'])
|
.get(pk=self.kwargs['family_pk'])
|
||||||
@ -183,6 +185,7 @@ class ChallengeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
"""
|
"""
|
||||||
model = Challenge
|
model = Challenge
|
||||||
extra_context = {"title": _('Create challenge')}
|
extra_context = {"title": _('Create challenge')}
|
||||||
|
form_class = ChallengeForm
|
||||||
|
|
||||||
def get_sample_object(self):
|
def get_sample_object(self):
|
||||||
return Challenge(
|
return Challenge(
|
||||||
@ -234,9 +237,43 @@ class ChallengeUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||||||
model = Challenge
|
model = Challenge
|
||||||
context_object_name = "challenge"
|
context_object_name = "challenge"
|
||||||
extra_context = {"title": _('Update challenge')}
|
extra_context = {"title": _('Update challenge')}
|
||||||
template_name = 'family/challenge_update.html'
|
form_class = ChallengeForm
|
||||||
form_class = ChallengeUpdateForm
|
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
def get_success_url(self, **kwargs):
|
||||||
self.object.refresh_from_db()
|
self.object.refresh_from_db()
|
||||||
return reverse_lazy('family:challenge_detail', kwargs={'pk': self.object.pk})
|
return reverse_lazy('family:challenge_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyManageView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
|
"""
|
||||||
|
Manage families and challenges
|
||||||
|
"""
|
||||||
|
model = Achievement
|
||||||
|
template_name = 'family/manage.html'
|
||||||
|
table_class = AchievementTable
|
||||||
|
extra_context = {'title': _('Manage families and challenges')}
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
# Check that the user is authenticated
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return self.handle_no_permission()
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self, **kwargs):
|
||||||
|
# retrieves only Transaction that user has the right to see.
|
||||||
|
return Achievement.objects.filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, Achievement, "view")
|
||||||
|
).order_by("-obtained_at").all()[:20]
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context['all_challenges'] = Challenge.objects.filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, Challenge, "view")
|
||||||
|
).order_by('name')
|
||||||
|
|
||||||
|
context["can_add_family"] = PermissionBackend.check_perm(self.request, "family.add_family")
|
||||||
|
context["can_add_challenge"] = PermissionBackend.check_perm(self.request, "family.add_challenge")
|
||||||
|
|
||||||
|
return context
|
||||||
|
Reference in New Issue
Block a user