Compare commits

...

131 commits

Author SHA1 Message Date
Martin Pépin
368285c92d Fix allauth-ens version, lol
↑
Merci Erkan

lol

super message de commit
2020-05-06 23:09:31 +02:00
Erkan Narmanli
177ecdc541 add storage to equipment 2019-03-18 14:01:44 +01:00
Daru13
2a0113412a Comment out the previously used event sorting method. 2018-11-26 18:38:24 +01:00
Daru13
b43422dc50 Make grouping by location an option, and set grid rows of event nodes.
Event nodes used to only be sorted. This commit explictly set their CSS 
grid rows.
2018-11-26 18:37:52 +01:00
Daru13
cd771c5ba5 Do not intersect an event with anotoher event it immediately follows.
This is required so that an event E1 ending at time T and an event E2 
starting at time T _can_ be assigned the same color (i.e. they _can_ 
appear on the same row).
2018-11-26 18:35:09 +01:00
Daru13
51251c0a8f Add a new event sorting algorithm (using interval graph coloring).
Note: this only sort the event nodes in a particular, "optimal" order; 
but it does not explicitly set the grid row of each node. Therefore, the 
result is likely to spread on many more rows than what could be 
expected.
2018-11-26 16:06:31 +01:00
Daru13
c4e70840ad Add a button to switch between calendar display modes.
It switches between:
* displaying all events;
* only displaying events the user subscribed to.

Note that the button has not been included in the calendar library/files 
(yet?). Its code should be moved somewhere else at a later time.
2018-11-25 16:08:43 +01:00
Daru13
81eb6bebac Allow to only display events the user subscribed to. 2018-11-25 16:05:00 +01:00
Qwann
9e7937d7d9 hotfix : héritage de has_perm 2018-11-24 17:07:46 +01:00
Qwann
40a9ce6531 mes perms 2018-11-24 16:53:54 +01:00
Daru13
6cc04e3792 Make events for which the user is enroled more distinguishable. 2018-11-24 06:01:00 +01:00
Daru13
ad02830521 Display a tooltip when an event content cannot be fully displayed.
This is intended to make it easy to have a glimpse at the content of 
an event, when its width is not sufficient for its content to be 
displayed (without overflowing its parent).
  
  It uses an external jQuery library to display tooltips: named _tipso_ 
(https://github.com/object505/tipso).
2018-11-24 05:59:13 +01:00
Daru13
aa06036968 Add the actual links to each event detailed page (in the detail popup).
Note that the URL format is passed to the calendar as a parameter (in 
the page template). 

It is currently hard-coded, but should be generated by Django instead!
2018-11-24 03:52:09 +01:00
Daru13
3c848fe47c Improve event detail popup positionning (no above-positionning yet).
The popup is horizontally aligned with the click location, and 
vertically close to event (slightly on top of it, from below).

If the click occurs too much on the left or the right side of the 
screen, the horizontal position is shifted accordingly, so that it is 
never displayed outside of the screen. Note, however, that the small 
arrow supposed to point the related event is not shifted as well as of 
now (not as straightforward since it is a CSS pseudo-element).

This commit also removes the ability to display the popup _above_ the 
event in case the click occurs too close to the bottom of the screen. 
This feature should be reintroduced by a later commit once it will have 
been fixed :).
2018-11-24 03:37:40 +01:00
Daru13
a0d158ca77 Add a clearer error message when the attempt to (un)enrol fails.
The error message returned by jQuery only contained "Error". This commit 
replaces it by a hard-coded, more descriptive error message in French.
2018-11-24 03:32:07 +01:00
Daru13
ab4338aa84 Rename the label of the event detail popup location field to "Lieu". 2018-11-24 03:29:38 +01:00
Daru13
a2a14cb84c Add dedicated classes to first/last (daily) time slots. 2018-11-24 03:28:25 +01:00
Daru13
ff5c0845bc Remove some useless, commented CSS properties. 2018-11-24 03:26:37 +01:00
Daru13
a029987159 Add some margin to the description and tags (in the event detail popup). 2018-11-24 03:26:10 +01:00
Daru13
72617946eb Fix the event detail popup table alignment (now centered). 2018-11-24 03:25:13 +01:00
Daru13
7a1e6c0be3 Prevent event detail popup dimension updates due to <h3> hovering. 2018-11-24 03:24:41 +01:00
Daru13
7217bd1862 Set new width constraints for the event detail popup. 2018-11-24 03:23:14 +01:00
Daru13
362c7400eb Improve the look of the first/last hour slots (for each day). 2018-11-24 03:22:33 +01:00
Daru13
a192765b03 Better align the time slot hours. 2018-11-24 03:21:19 +01:00
Daru13
49e147e620 Make the whole calendar inherit the font family from its parent. 2018-11-24 03:20:52 +01:00
Daru13
5e9423c858 Fix the wrong font size of weveral calendar elements. 2018-11-24 03:19:57 +01:00
Daru13
eb8f82f6c4 Display the calendar in full height (w/ scroll). 2018-11-24 03:18:01 +01:00
Daru13
41b640a1ea Ensure time slot and event grids have the same number of columns. 2018-11-24 03:14:18 +01:00
Daru13
b2dabd1dc4 Fix event positionning in the grid. 2018-11-24 03:09:19 +01:00
Qwann
504044b1cf hotfix: inheritance in calendar 2018-11-23 15:33:04 +01:00
Qwann
bd84b46476 Merge branch 'Qwann/calendar' of git.eleves.ens.fr:klub-dev-ens/GestionEvenementiel into Qwann/calendar 2018-11-22 22:55:30 +01:00
Qwann
c30f588bed Merge branch 'master' of git.eleves.ens.fr:klub-dev-ens/GestionEvenementiel into Qwann/calendar 2018-11-22 22:55:17 +01:00
Daru13
74eaf82575 Fix the structure of the HTML of the generated events. 2018-11-22 22:43:18 +01:00
Daru13
12534ff2da Update the start and end date of the calendar (displayed era). 2018-11-22 22:42:31 +01:00
Daru13
2699573913 Fix the title of the event (used to be the event id). 2018-11-22 22:42:09 +01:00
Daru13
7a7254f3da Put the event title on its own line (in the detail popup). 2018-11-22 22:36:23 +01:00
Daru13
5da4c7050c Updates the perm counter on perm area update. 2018-11-22 22:34:45 +01:00
Daru13
555fa8fb7c Adds effective enrol/unerol POST request on related button click. 2018-11-22 22:34:09 +01:00
Qwann
1865054c29 waaaa, maintenant on peut genre se connecter 2018-11-22 13:24:59 +01:00
Evarin
3ebb89c1c6 Calendrier accessible et presque fonctionnel 2018-11-22 00:39:12 +01:00
Evarin
350836aeb2 Revert to old_css 2018-11-21 19:46:41 +01:00
Daru13
d08a39307f Initial addition of the visual calendar (day view only).
This is the first step to include a visual calendar to Poulpe,
only including a "day view" as of now (i.e. events hour-by-hour from one date to another).
More views may be added at a later time.

It is **NOT WORKING YET**!
The CSS has been broken on this branch, and will have to be fixed before the calendar can work:
* the CSS of the calendar needs to be adapted to the environment and design of Poulpe;
* add actual links to enroll/un-enroll to an activity (cf. `Event` class in `calendar.js`);
* other small tweaks :)?

Finally, this view is likely to require the addition of start and end date change,
so that an user can browse events over several days (cf. `setStartDate` and `setEndDate` methods of `Calendar` class in `calendar.js`).

Note that this code should be better re-written (e.g. in Typescript, split between files, using more/better design patterns) at a later time.
It should nonetheless be easy to fix it and use it right now (see above requirements for this).
2018-11-19 23:57:42 +01:00
Daru13
d3e1943021 Merge branch 'Evarin/enrol-json' into Qwann/calendar 2018-11-19 22:32:58 +01:00
Evarin
511c3096f7 enrol?ajax=json renvoie un JSON pour le calendrier dynamique 2018-11-12 21:44:10 +01:00
Qwann
b1c0dd857d search matos 2018-11-07 21:39:50 +01:00
Qwann
abdd893309 dos not work 2018-10-12 18:04:27 +02:00
Qwann
1042b9f9e4 partial commit 2018-10-12 17:15:55 +02:00
Qwann
94a629371c allauth don't works right now… 2018-10-11 09:48:03 +02:00
Qwann
a32881a24b MAJ allauth 2018-10-04 21:34:39 +02:00
Qwann
7ebee733c5 css no finished 2018-10-04 21:10:30 +02:00
Evarin
601ba4d116 Fix css filtrage catégories 2018-08-28 22:40:02 +02:00
Evarin
6d81735a55 Inscription en perm 2018-08-28 22:19:24 +02:00
Qwann
634c4ad4ff hotfix: admin 2018-08-27 17:11:27 +02:00
Qwann
727bd10aeb hotfix: place and tags show correctly 2018-08-27 15:51:29 +02:00
Qwann
3275de7c9c hotfix: dates activity 2018-08-27 15:31:44 +02:00
Qwann
dd47cc3b07 avec les migrations c'est mieux 2018-08-27 14:09:27 +02:00
Qwann
62330956ec MAJ : MAJ 2.1, remove user urls 2018-08-27 14:03:06 +02:00
Qwann
c8717fb06b MAJ : preparing for Django 3.0 2018-08-27 00:56:57 +02:00
Qwann
3c9486a858 Merge branch 'Qwann/django_maj' 2018-08-27 00:31:28 +02:00
Qwann
906db7069b MAJ : MAJ 2.0, remove debug_panel, django.change url.resolvers to django.urls 2018-08-27 00:30:12 +02:00
Qwann
fc0f861c2c avec les migrations c'est mieux 2018-08-27 00:09:41 +02:00
Qwann
b45d7722a7 MAJ : add on_delete + MIDDLEWARE 2018-08-27 00:02:33 +02:00
Qwann
7c7adab658 event base done 2018-08-26 21:18:51 +02:00
Qwann
7ebc34d5aa date behaviour is correct 2018-08-22 14:04:42 +02:00
Erkan Narmanli
c723f53909 Merge branch 'Qwann/events/admin' into 'master'
Qwann/events/admin

See merge request cof-geek/GestionEvenementiel!28
2018-08-20 18:23:10 +02:00
Qwann
5d39b1018b admin for event 2018-08-20 18:20:18 +02:00
Qwann
0ff2f40832 cleaning activity 2018-08-20 17:46:41 +02:00
Qwann
e6d79df735 event admin base 2018-08-20 16:02:42 +02:00
Qwann
7690c1a8ab hotfix: owner can be None 2018-08-20 15:18:54 +02:00
Qwann
7064e89059 event admin 2018-08-20 15:16:40 +02:00
Erkan Narmanli
20fad42d6d Merge branch 'Qwann/mv_root' into 'master'
rename app evenementiel to poulpe

See merge request cof-geek/GestionEvenementiel!27
2018-08-20 14:36:12 +02:00
Qwann
e8c1ecadcc rename app evenementiel to poulpe 2018-08-20 14:32:52 +02:00
Qwann
fb1308e70c add migrations 2018-08-20 13:24:47 +02:00
Erkan Narmanli
617edf8691 Merge branch 'Qwann/inventaire_list' into 'master'
equipment main views

See merge request cof-geek/GestionEvenementiel!26
2018-08-20 13:22:07 +02:00
Qwann
fd4567b531 equipment main views 2018-08-20 13:17:10 +02:00
Qwann
df0d5b4351 email_server 2018-08-11 13:47:25 +02:00
Qwann
44c003ea7d Merge branch 'master' into Qwann/inventaire 2018-08-11 13:30:59 +02:00
Qwann
573df9e3f7 admin modified 2018-08-09 18:34:36 +02:00
Qwann
57e3feb414 it is possible not to loose an item 2018-08-09 17:57:13 +02:00
Qwann
8dee36f928 new admin ! 2018-08-09 17:52:28 +02:00
Qwann
6e993c0e27 admin search fields 2018-08-09 16:14:28 +02:00
Qwann
14728c8513 category full name 2018-08-09 16:06:24 +02:00
Qwann
ec8e289ff8 no cyclic reference possible 2018-08-09 15:11:20 +02:00
Qwann
174e62b316 rename stock verbose 2018-08-09 14:36:12 +02:00
Qwann
b063d18cf0 equipement added_at modifed_at 2018-08-09 14:06:40 +02:00
Qwann
ae0fac4b86 new poulpe 2018-08-09 10:56:29 +02:00
Qwann
5b2207bd59 cascade or protect set 2018-08-08 16:56:24 +02:00
Qwann
70d1d403f6 equipment attributes 2018-08-08 16:45:29 +02:00
Qwann
c8f4ab1a80 categories as taxinomy 2018-08-08 16:02:23 +02:00
Qwann
4c1a1da64f add group owner 2018-08-07 20:44:57 +02:00
Qwann
a5cb99c0c3 rm old owner 2018-08-07 20:43:56 +02:00
Qwann
f361932590 site name 2018-08-07 20:29:56 +02:00
Qwann
c8f5df3a13 change owner to group 2018-08-07 20:29:37 +02:00
Qwann
788a2077e2 change remarks, add lost 2018-08-07 19:08:20 +02:00
Qwann
41883cb2dd equipment admin list 2018-08-07 16:26:33 +02:00
Qwann
4bfb11bdd3 collaps admin 2018-08-07 15:58:38 +02:00
Qwann
707ef2292f add cof-geek site 2018-08-07 10:00:43 +02:00
Qwann
e5ac994380 small fixes 2018-08-06 19:31:34 +02:00
Evarin
747d3419b2 Fix IdField validation 2018-08-06 18:34:07 +02:00
Qwann
7ab4dcede0 owner+pole+category-tags 2018-08-06 17:06:38 +02:00
Qwann
ecdcc0e949 urls fixed 2018-08-06 16:15:09 +02:00
Qwann
b07cddaf3b life is crap 2018-08-06 16:12:33 +02:00
Qwann
7215da1d81 I kwew it was you python-cas! 2018-08-06 11:58:54 +02:00
Qwann
31cde9ca26 prod requirements 2018-08-06 10:19:48 +02:00
Qwann
e863823457 init migrations for prod 2018-08-06 09:52:56 +02:00
Qwann
e6c0a0697e ignore redis & channels for the time being 2018-08-06 09:45:14 +02:00
Qwann
0c32b05a48 ignore sass-cache 2018-08-05 15:04:49 +02:00
Qwann
6b2ea6eae4 CAS not working 2018-08-05 15:01:51 +02:00
Qwann
556e59706f mmmh 2018-08-05 14:26:07 +02:00
Qwann
f2c65b11e1 ça fonctionne bien 2018-08-04 18:18:34 +02:00
Qwann
0483610e69 moins caca 2018-08-04 05:38:03 +02:00
Qwann
0be9e5eb3a caca 2018-08-02 17:55:27 +02:00
Qwann
123d524eab start equipement 2018-07-26 15:53:42 +02:00
Qwann
328f3d7852 package dependencies 2018-07-13 14:54:38 +02:00
Martin Pepin
3aab76613a Merge branch 'Aufinal/permissions' into 'master'
Permissions par évènement

See merge request cof-geek/GestionEvenementiel!15
2018-05-03 14:34:36 +02:00
Martin Pépin
10e4393bda Merge branch 'master' into Aufinal/permissions 2018-05-03 12:45:29 +02:00
Martin Pépin
94b2529ab6 Use the correct user model when creating users 2018-05-03 12:38:31 +02:00
Martin Pépin
306ca2bcc8 switch to channels 2.x 2018-05-03 12:34:23 +02:00
Martin Pépin
ce9ee193d8 asgi-redis has been renamed to channels_redis 2018-05-03 12:25:37 +02:00
Ludovic Stephan
392e5cf144 Revert c894d35 --- Fuck django-guardian
This reverts commit c894d35932.
2017-09-19 11:48:39 +02:00
Ludovic Stephan
c894d35932 Scrap useless perms 2017-09-19 10:46:26 +02:00
Erkan Narmanli
cbd5a7db0b Merge branch 'aureplop/appconfigs' into 'master'
Update AppConfigs

See merge request !23
2017-08-30 21:22:05 +02:00
Aurélien Delobelle
e1b4dc1651 Update AppConfigs 2017-08-19 15:36:45 +02:00
Ludovic Stephan
0b1641a002 Tests and group uniqueness 2017-08-17 15:10:46 +02:00
Ludovic Stephan
64a979a4ac Merge branch 'master' of git.eleves.ens.fr:cof-geek/GestionEvenementiel into Aufinal/permissions 2017-08-17 14:23:13 +02:00
Ludovic Stephan
25df34e57e Add filter to permissions 2017-07-24 15:30:16 +02:00
Ludovic Stephan
910d8fe9c0 Simplify flag system 2017-07-21 21:06:45 +02:00
Ludovic Stephan
e2d5e726cd Finish dj-guardian setup 2017-07-21 20:56:49 +02:00
Ludovic Stephan
e499281a1d Create permissions and signals 2017-07-21 20:24:09 +02:00
Ludovic Stephan
6ece994f1c Merge branch 'master' of git.eleves.ens.fr:cof-geek/GestionEvenementiel into Aufinal/permissions 2017-07-21 17:02:21 +02:00
Ludovic Stephan
0093956696 Add event-specific groups and signals 2017-07-21 15:49:33 +02:00
186 changed files with 9738 additions and 8965 deletions

3
.gitignore vendored
View file

@ -1,7 +1,8 @@
.vagrant/ .vagrant/
__pycache__ __pycache__
venv venv
evenementiel/settings.py poulpe/settings.py
.*.swp .*.swp
*.pyc *.pyc
*.sqlite3 *.sqlite3
*.scssc

View file

@ -0,0 +1 @@
default_app_config = 'api.apps.APIConfig'

7
api/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class APIConfig(AppConfig):
name = 'api'
verbose_name = _("API")

View file

@ -0,0 +1 @@
default_app_config = 'communication.apps.CommunicationConfig'

View file

@ -1,4 +1,7 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class CommunicationConfig(AppConfig): class CommunicationConfig(AppConfig):
name = 'communication' name = 'communication'
verbose_name = _("Communication")

View file

@ -20,7 +20,10 @@ class Subscription(models.Model):
class UserSubscription(Subscription): class UserSubscription(Subscription):
user = models.ForeignKey(User) user = models.ForeignKey(
User,
on_delete=models.CASCADE,
)
is_unsub = models.BooleanField( is_unsub = models.BooleanField(
_("désinscription"), _("désinscription"),
default=False default=False
@ -32,7 +35,10 @@ class UserSubscription(Subscription):
class GroupSubscription(Subscription): class GroupSubscription(Subscription):
group = models.ForeignKey(Group) group = models.ForeignKey(
Group,
on_delete=models.CASCADE,
)
class Meta: class Meta:
verbose_name = _("souscription en groupe") verbose_name = _("souscription en groupe")

1
equipment/__init__.py Normal file
View file

@ -0,0 +1 @@
default_app_config = 'equipment.apps.EquipmentConfig'

133
equipment/admin.py Normal file
View file

@ -0,0 +1,133 @@
from django.contrib import admin
from django import forms
from .models import Equipment, EquipmentDefault, EquipmentRevision, EquipmentCategory, EquipmentLost, EquipmentAttributeValue, EquipmentAttribute, EquipmentStorage
from .fields import IdField, IdWidget
class IdForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
if 'min_value' in kwargs:
kwargs.pop('min_value')
if 'num_choices' in kwargs:
num_choices = kwargs.pop('num_choices')
else:
num_choices = None
super(IdForm, self).__init__(*args, **kwargs)
for field in self.instance._meta.fields:
if isinstance(field, IdField):
if num_choices is None:
choices = []
else:
choices = [(k, str(k)) for k in range(1, num_choices+1)]
self.fields[field.name].choices = choices
self.fields[field.name].widget = IdWidget(choices=self.fields[field.name].choices)
class IdFormset(forms.models.BaseInlineFormSet):
def get_form_kwargs(self, index):
kwargs = super().get_form_kwargs(index)
if self.instance:
kwargs["num_choices"] = self.instance.stock
return kwargs
class EquipmentRevisionExtraInline(admin.TabularInline):
model = EquipmentRevision
extra = 0
form = IdForm
formset = IdFormset
classes = ['collapse']
class EquipmentDefaultExtraInline(admin.TabularInline):
model = EquipmentDefault
extra = 0
form = IdForm
formset = IdFormset
classes = ['collapse']
class EquipmentLostExtraInline(admin.TabularInline):
model = EquipmentLost
extra = 0
form = IdForm
formset = IdFormset
classes = ['collapse']
class EquipmentAttributeValueInline(admin.TabularInline):
model = EquipmentAttributeValue
extra = 0
class CategoryAdmin(admin.ModelAdmin):
readonly_fields = ['full_name_p']
list_display = ['name', 'parent', "full_name_p"]
ordering = ['name', 'parent']
search_fields = ['name',]
autocomplete_fields = ['parent', ]
class StorageAdmin(admin.ModelAdmin):
list_display = ['name']
ordering = ['name']
search_fields = ['name']
class EquipmentAttributeAdmin(admin.ModelAdmin):
list_display = ['name']
ordering = ['name']
search_fields = ['name',]
class EquipmentAdmin(admin.ModelAdmin):
save_as_continue = True
save_on_top = True
autocomplete_fields = ['category', 'storage', ]
readonly_fields = ['full_category_p',
'added_at',
'modified_at',
'stock_aviable_p',
'ids_aviable_p',
'stock_lost_p',
'ids_lost_p',
]
list_display = ['name', 'stock', 'owner', 'category', 'storage', 'modified_at']
fieldsets = (
('Général', {
'fields': ('name', 'owner', 'stock', )
}),
('Info stock',
{
'fields': (
'stock_aviable_p',
'ids_aviable_p',
'stock_lost_p',
'ids_lost_p',
),
}),
('Attributs', {
'fields': ('category', 'full_category_p', 'storage'),
}),
('Description', {
'fields': ('description', 'added_at', 'modified_at',),
}),
)
ordering = ['name', 'owner', 'category']
inlines = [EquipmentAttributeValueInline,
EquipmentDefaultExtraInline,
EquipmentLostExtraInline,
EquipmentRevisionExtraInline]
search_fields = ['name', 'description',]
list_filter = ['owner', 'category', 'storage', ]
admin.site.register(Equipment, EquipmentAdmin)
admin.site.register(EquipmentCategory, CategoryAdmin)
admin.site.register(EquipmentStorage, StorageAdmin)
admin.site.register(EquipmentAttribute, EquipmentAttributeAdmin)

View file

@ -1,5 +1,7 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class EquipmentConfig(AppConfig): class EquipmentConfig(AppConfig):
name = 'equipment' name = 'equipment'
verbose_name = _("Équipement")

77
equipment/fields.py Normal file
View file

@ -0,0 +1,77 @@
from django.db import models
from django import forms
#class IdWidget(AdminMultipleChoiceFieldWidget):
class IdWidget(forms.widgets.CheckboxSelectMultiple):
template_name = 'equipment/widgets/checkbox_select.html'
option_template_name = 'equipment/widgets/checkbox_option.html'
def __init__(self, *args, **kwargs):
# nb_items = kwargs.pop('nb_items')
# kwargs['choices'] = list(range(1, nb_items+1))
super(IdWidget, self).__init__(*args, **kwargs)
class Media:
css = {
'all': ('css/idwidget.css',)
}
class IdFormField(forms.MultipleChoiceField):
#widget = IdWidget
def __init__(self, *args, **kwargs):
if 'min_value' in kwargs:
kwargs.pop('min_value')
if 'max_value' in kwargs:
kwargs.pop('max_value')
super(IdFormField, self).__init__(*args, **kwargs)
class IdField(models.BigIntegerField):
def parse_integer(self, n):
res = []
k = 1
while(n > 0):
if n & 1:
res.append(k)
n >>= 1
k += 1
return res
def from_db_value(self, value, expression, connection):
if value is None:
return value
return self.parse_integer(value)
def to_python(self, value):
if isinstance(value, list):
return value
if value is None:
return value
return self.parse_integer(value)
def get_prep_value(self, value):
res = 0
for b in value:
res |= 1 << (int(b)-1)
return res
def __init__(self, separator=",", *args, **kwargs):
self.separator = separator
super(IdField, self).__init__(*args, **kwargs)
self.validators = [] # TODO : validateurs pertinents
def deconstruct(self):
name, path, args, kwargs = super(IdField, self).deconstruct()
# Only include kwarg if it's not the default
if self.separator != ",":
kwargs['separator'] = self.separator
return name, path, args, kwargs
def formfield(self, **kwargs):
# This is a fairly standard way to set up some defaults
# while letting the caller override them.
defaults = {'form_class': IdFormField}
defaults.update(kwargs)
return super(IdField, self).formfield(**defaults)

View file

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-12 12:47 # Generated by Django 1.11.11 on 2018-08-06 17:29
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import equipment.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -32,7 +34,7 @@ class Migration(migrations.Migration):
name='EquipmentAttribution', name='EquipmentAttribution',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.PositiveSmallIntegerField(verbose_name='quantité attribuée')), ('amount', models.BigIntegerField(verbose_name='quantité attribuée')),
('remarks', models.TextField(verbose_name="remarques concernant l'attribution")), ('remarks', models.TextField(verbose_name="remarques concernant l'attribution")),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Activity')), ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Activity')),
('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.Equipment')), ('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.Equipment')),
@ -42,12 +44,45 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'attributions de matériel', 'verbose_name_plural': 'attributions de matériel',
}, },
), ),
migrations.CreateModel(
name='EquipmentCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='nom')),
],
options={
'verbose_name': 'catégories',
'verbose_name_plural': 'catégories',
},
),
migrations.CreateModel(
name='EquipmentOwner',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='nom')),
],
options={
'verbose_name': 'propriétaire de matériel',
'verbose_name_plural': 'propriétaires de matériel',
},
),
migrations.CreateModel(
name='EquipmentPole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='nom')),
],
options={
'verbose_name': 'pôle',
'verbose_name_plural': 'pôle',
},
),
migrations.CreateModel( migrations.CreateModel(
name='EquipmentRemark', name='EquipmentRemark',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('remark', models.TextField(verbose_name='remarque sur le matériel')), ('remark', models.TextField(verbose_name='remarque sur le matériel')),
('amount', models.PositiveSmallIntegerField(verbose_name='quantité concernée')), ('ids', equipment.fields.IdField()),
('is_broken', models.BooleanField()), ('is_broken', models.BooleanField()),
('is_lost', models.BooleanField()), ('is_lost', models.BooleanField()),
('equipment', models.ForeignKey(help_text='Matériel concerné par la remarque', on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='equipment.Equipment')), ('equipment', models.ForeignKey(help_text='Matériel concerné par la remarque', on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='equipment.Equipment')),
@ -57,14 +92,43 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'remarques sur le matériel', 'verbose_name_plural': 'remarques sur le matériel',
}, },
), ),
migrations.CreateModel(
name='EquipmentRevision',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=datetime.date.today, verbose_name='date')),
('remark', models.TextField(verbose_name='remarque sur la révision')),
('ids', equipment.fields.IdField()),
('equipment', models.ForeignKey(help_text='Matériel concerné par les révisions', on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='equipment.Equipment')),
],
options={
'verbose_name': 'révision de matériel',
'verbose_name_plural': 'révisions de matériel',
},
),
migrations.AddField( migrations.AddField(
model_name='equipment', model_name='equipment',
name='activities', name='activities',
field=models.ManyToManyField(related_name='equipment', through='equipment.EquipmentAttribution', to='event.Activity'), field=models.ManyToManyField(related_name='equipment', through='equipment.EquipmentAttribution', to='event.Activity'),
), ),
migrations.AddField(
model_name='equipment',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentCategory'),
),
migrations.AddField( migrations.AddField(
model_name='equipment', model_name='equipment',
name='event', name='event',
field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'), field=models.ForeignKey(blank=True, help_text="Si spécifié, l'instance du modèle est spécifique à l'évènement en question.", null=True, on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'),
), ),
migrations.AddField(
model_name='equipment',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentOwner'),
),
migrations.AddField(
model_name='equipment',
name='pole',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentPole'),
),
] ]

View file

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-07 16:58
from __future__ import unicode_literals
import datetime
from django.db import migrations, models
import django.db.models.deletion
import equipment.fields
class Migration(migrations.Migration):
dependencies = [
('equipment', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='EquipmentDefault',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('remark', models.TextField(verbose_name='remarque sur le défaut')),
('ids', equipment.fields.IdField()),
('is_unusable', models.BooleanField(verbose_name='inutilisable')),
('send_repare', models.BooleanField(verbose_name='à envoyer réparareur')),
('equipment', models.ForeignKey(help_text='Matériel concerné par le defaut', on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='equipment.Equipment')),
],
options={
'verbose_name': 'defaut matériel',
'verbose_name_plural': 'défauts sur le matériel',
},
),
migrations.CreateModel(
name='EquipmentLost',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('lost_at', models.DateField(default=datetime.date.today, verbose_name='perdu le')),
('ids', equipment.fields.IdField()),
('equipment', models.ForeignKey(help_text='Matériel concerné par la perte', on_delete=django.db.models.deletion.CASCADE, related_name='losts', to='equipment.Equipment')),
],
),
migrations.RemoveField(
model_name='equipmentremark',
name='equipment',
),
migrations.DeleteModel(
name='EquipmentRemark',
),
]

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-07 18:43
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('equipment', '0002_auto_20180807_1658'),
]
operations = [
migrations.RemoveField(
model_name='equipment',
name='owner',
),
migrations.DeleteModel(
name='EquipmentOwner',
),
]

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-07 18:44
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('auth', '0008_alter_user_username_max_length'),
('equipment', '0003_auto_20180807_1843'),
]
operations = [
migrations.AddField(
model_name='equipment',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.Group'),
),
]

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-08 10:13
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('equipment', '0004_equipment_owner'),
]
operations = [
migrations.RemoveField(
model_name='equipment',
name='pole',
),
migrations.AddField(
model_name='equipmentcategory',
name='parent',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentCategory'),
),
migrations.DeleteModel(
name='EquipmentPole',
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-08 13:54
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('equipment', '0005_auto_20180808_1013'),
]
operations = [
migrations.AlterField(
model_name='equipmentcategory',
name='parent',
field=models.ForeignKey(blank=True, default=None, help_text='merci de ne pas faire de référence cyclique', null=True, on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentCategory'),
),
]

View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-08 14:40
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('equipment', '0006_auto_20180808_1354'),
]
operations = [
migrations.CreateModel(
name='EquipmentAttribute',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, unique=True, verbose_name='nom')),
],
options={
'verbose_name': 'attribut',
'verbose_name_plural': 'attributs',
},
),
migrations.CreateModel(
name='EquipmentAttributeValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.CharField(max_length=200, verbose_name='valeur')),
('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentAttribute')),
('equipment', models.ForeignKey(help_text='Matériel concerné par le defaut', on_delete=django.db.models.deletion.CASCADE, related_name='attributes', to='equipment.Equipment')),
],
options={
'verbose_name': 'attribut de matériel',
'verbose_name_plural': 'attributs de matériel',
},
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-08 14:54
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('equipment', '0007_equipmentattribute_equipmentattributevalue'),
]
operations = [
migrations.AlterField(
model_name='equipment',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='equipment.EquipmentCategory'),
),
]

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-09 12:00
from __future__ import unicode_literals
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('equipment', '0008_auto_20180808_1454'),
]
operations = [
migrations.AddField(
model_name='equipment',
name='added_at',
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2018, 8, 9, 12, 0, 50, 140250, tzinfo=utc), verbose_name='ajouté le'),
preserve_default=False,
),
migrations.AddField(
model_name='equipment',
name='modified_at',
field=models.DateTimeField(auto_now=True, verbose_name='dernière modification'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-09 12:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('equipment', '0009_auto_20180809_1200'),
]
operations = [
migrations.AlterField(
model_name='equipment',
name='stock',
field=models.PositiveSmallIntegerField(verbose_name='quantité totale'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-09 14:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('equipment', '0010_auto_20180809_1211'),
]
operations = [
migrations.AlterField(
model_name='equipment',
name='description',
field=models.TextField(blank=True, verbose_name='description'),
),
]

View file

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-20 11:24
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('equipment', '0011_auto_20180809_1405'),
]
operations = [
migrations.AlterField(
model_name='equipment',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='equipment.EquipmentCategory', verbose_name='catégorie'),
),
migrations.AlterField(
model_name='equipment',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.Group', verbose_name='propriétaire'),
),
migrations.AlterField(
model_name='equipmentattributevalue',
name='attribute',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.EquipmentAttribute', verbose_name='attribut'),
),
migrations.AlterField(
model_name='equipmentattributevalue',
name='equipment',
field=models.ForeignKey(help_text='Matériel concerné par le defaut', on_delete=django.db.models.deletion.CASCADE, related_name='attributes', to='equipment.Equipment', verbose_name='matériel'),
),
migrations.AlterField(
model_name='equipmentattribution',
name='equipment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='equipment.Equipment', verbose_name='matériel'),
),
migrations.AlterField(
model_name='equipmentcategory',
name='parent',
field=models.ForeignKey(blank=True, default=None, help_text='merci de ne pas faire de référence cyclique', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='equipment.EquipmentCategory', verbose_name='parent'),
),
migrations.AlterField(
model_name='equipmentdefault',
name='equipment',
field=models.ForeignKey(help_text='Matériel concerné par le defaut', on_delete=django.db.models.deletion.CASCADE, related_name='remarks', to='equipment.Equipment', verbose_name='matériel'),
),
migrations.AlterField(
model_name='equipmentlost',
name='equipment',
field=models.ForeignKey(help_text='Matériel concerné par la perte', on_delete=django.db.models.deletion.CASCADE, related_name='losts', to='equipment.Equipment', verbose_name='matériel'),
),
migrations.AlterField(
model_name='equipmentrevision',
name='equipment',
field=models.ForeignKey(help_text='Matériel concerné par les révisions', on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='equipment.Equipment', verbose_name='matériel'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-26 17:49
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('equipment', '0012_auto_20180820_1124'),
]
operations = [
migrations.AlterField(
model_name='equipmentattribution',
name='remarks',
field=models.TextField(blank=True, verbose_name="remarques concernant l'attribution"),
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-26 22:05
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('equipment', '0013_auto_20180826_1949'),
]
operations = [
migrations.AlterField(
model_name='equipment',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.Group', verbose_name='propriétaire'),
),
migrations.AlterField(
model_name='equipmentcategory',
name='parent',
field=models.ForeignKey(blank=True, default=None, help_text='merci de ne pas faire de référence cyclique', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='equipment.EquipmentCategory', verbose_name='parent'),
),
]

View file

@ -0,0 +1,26 @@
# Generated by Django 2.1.5 on 2019-03-18 11:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('equipment', '0014_auto_20180827_0005'),
]
operations = [
migrations.CreateModel(
name='EquipmentStorage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='nom')),
],
),
migrations.AddField(
model_name='equipment',
name='storage',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='equipment.EquipmentStorage', verbose_name='stockage'),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 2.1.5 on 2019-03-18 11:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('equipment', '0015_auto_20190318_1245'),
]
operations = [
migrations.AlterModelOptions(
name='equipmentstorage',
options={'verbose_name': 'stockage', 'verbose_name_plural': 'stockages'},
),
]

View file

@ -1,7 +1,98 @@
from django.db import models from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import Group
from event.models import Activity, EventSpecificMixin from event.models import Activity, EventSpecificMixin
from django.db.models import Q
from .fields import IdField
from datetime import date
class EquipmentStorage(models.Model):
name = models.CharField(
_("nom"),
max_length=200,
)
class Meta:
verbose_name = _("stockage")
verbose_name_plural = _("stockages")
def __str__(self):
return self.name
class EquipmentCategory(models.Model):
name = models.CharField(
_("nom"),
max_length=200,
)
parent = models.ForeignKey(
'self',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
related_name="children",
help_text=_("merci de ne pas faire de référence cyclique"),
verbose_name=_("parent"),
)
def has_parent(self, cat):
current = self
for k in range(100):
if current is None:
return False
if current == cat:
return True
current = current.parent
def full_name(self):
current = self
res = ""
for k in range(100):
res = "/{current}{old}".format(
current=current.name,
old=res)
if current.parent is None:
break
current = current.parent
return res
full_name.short_description = _("Chemin complet")
full_name_p = property(full_name)
class Meta:
verbose_name = _("catégories")
verbose_name_plural = _("catégories")
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if self.pk:
done = False
current = self
while not done:
if current.parent == self:
self.parent = None
done = True
elif current.parent is None:
done = True
current = current.parent
return super().save(*args, **kwargs)
class EquipmentQuerySet(models.QuerySet):
def in_category(self, cat):
filtre = Q(id__lt=0)
childs_id = [c.id for c in EquipmentCategory.objects.all()
if c.has_parent(cat)]
for pk in childs_id:
filtre |= Q(category__id=pk)
return self.filter(filtre)
class Equipment(EventSpecificMixin, models.Model): class Equipment(EventSpecificMixin, models.Model):
@ -9,13 +100,98 @@ class Equipment(EventSpecificMixin, models.Model):
_("nom du matériel"), _("nom du matériel"),
max_length=200, max_length=200,
) )
stock = models.PositiveSmallIntegerField(_("quantité disponible")) stock = models.PositiveSmallIntegerField(_("quantité totale"))
description = models.TextField(_("description")) description = models.TextField(
_("description"),
blank=True,
)
activities = models.ManyToManyField( activities = models.ManyToManyField(
Activity, Activity,
related_name="equipment", related_name="equipment",
through="EquipmentAttribution", through="EquipmentAttribution",
) )
owner = models.ForeignKey(
Group,
verbose_name=_("propriétaire"),
blank=True,
null=True,
on_delete=models.SET_NULL,
)
category = models.ForeignKey(
EquipmentCategory,
verbose_name=_("catégorie"),
on_delete=models.PROTECT,
)
storage = models.ForeignKey(
EquipmentStorage,
blank=True,
null=True,
default=None,
verbose_name=_("stockage"),
on_delete=models.PROTECT,
)
added_at = models.DateTimeField(
_("ajouté le"),
auto_now_add=True,
)
modified_at = models.DateTimeField(
_("dernière modification"),
auto_now=True,
)
objects = EquipmentQuerySet.as_manager()
def is_in_category(self, cat):
current = self.category
for k in range(100):
if current is None:
return False
if current == cat:
return True
current = current.parent
def ids_aviable(self):
if self.stock is None:
return []
res = list(map(lambda x: x+1, range(self.stock)))
for lost in self.losts.all():
res = [x
for x in res
if x not in lost.ids]
# TODO cassé
# TODO utilisés
return res
def ids_lost(self):
res = []
for lost in self.losts.all():
res = res + [x
for x in lost.ids
if x not in res]
return res
def stock_aviable(self):
aviable = self.ids_aviable()
return len(aviable)
def stock_lost(self):
return len(self.ids_lost())
def full_category(self):
return self.category.full_name()
full_category.short_description = _("Chemin complet")
ids_aviable.short_description = _("disponibles")
ids_lost.short_description = _("perdus")
stock_aviable.short_description = _("quantité disponible")
stock_lost.short_description = _("quantité perdue")
full_category_p = property(full_category)
ids_aviable_p = property(ids_aviable)
ids_lost_p = property(ids_lost)
stock_aviable_p = property(stock_aviable)
stock_lost_p = property(stock_lost)
class Meta: class Meta:
verbose_name = _("matériel") verbose_name = _("matériel")
@ -25,11 +201,63 @@ class Equipment(EventSpecificMixin, models.Model):
return self.name return self.name
class EquipmentAttribute(models.Model):
name = models.CharField(
_("nom"),
max_length=200,
unique=True,
)
class Meta:
verbose_name = _("attribut")
verbose_name_plural = _("attributs")
def __str__(self):
return self.name
class EquipmentAttributeValue(models.Model):
equipment = models.ForeignKey(
Equipment,
on_delete=models.CASCADE,
related_name="attributes",
verbose_name=_("matériel"),
help_text=_("Matériel concerné par le defaut"),
)
attribute = models.ForeignKey(
EquipmentAttribute,
verbose_name=_("attribut"),
on_delete=models.CASCADE,
)
value = models.CharField(
_("valeur"),
max_length=200,
)
class Meta:
verbose_name = _("attribut de matériel")
verbose_name_plural = _("attributs de matériel")
def __str__(self):
return "{attr}={value}".format(attr=self.attribute.name,
value=self.value)
class EquipmentAttribution(models.Model): class EquipmentAttribution(models.Model):
equipment = models.ForeignKey(Equipment) equipment = models.ForeignKey(
activity = models.ForeignKey(Activity) Equipment,
amount = models.PositiveSmallIntegerField(_("quantité attribuée")) verbose_name=_("matériel"),
remarks = models.TextField(_("remarques concernant l'attribution")) on_delete=models.CASCADE,
)
activity = models.ForeignKey(
Activity,
on_delete=models.CASCADE,
)
amount = models.BigIntegerField(_("quantité attribuée"))
remarks = models.TextField(
_("remarques concernant l'attribution"),
blank=True,
)
class Meta: class Meta:
verbose_name = _("attribution de matériel") verbose_name = _("attribution de matériel")
@ -37,30 +265,72 @@ class EquipmentAttribution(models.Model):
def __str__(self): def __str__(self):
return "%s (%d) -> %s" % (self.equipment.name, return "%s (%d) -> %s" % (self.equipment.name,
self.amout, self.amount,
self.activity.get_herited('title')) self.activity.get_herited('title'))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.equipment.event and self.equipment.event != self.activity.event: if (self.equipment.event
and self.equipment.event != self.activity.event):
raise ValidationError raise ValidationError
super(EquipmentAttribution, self).save(*args, **kwargs) super(EquipmentAttribution, self).save(*args, **kwargs)
class EquipmentRemark(models.Model): class EquipmentDefault(models.Model):
remark = models.TextField(_("remarque sur le matériel")) remark = models.TextField(_("remarque sur le défaut"))
equipment = models.ForeignKey( equipment = models.ForeignKey(
Equipment, Equipment,
verbose_name=_("matériel"),
on_delete=models.CASCADE,
related_name="remarks", related_name="remarks",
help_text=_("Matériel concerné par la remarque"), help_text=_("Matériel concerné par le defaut"),
) )
amount = models.PositiveSmallIntegerField(_("quantité concernée")) ids = IdField()
is_broken = models.BooleanField() is_unusable = models.BooleanField(_("inutilisable"))
is_lost = models.BooleanField() send_repare = models.BooleanField(_("à envoyer réparareur"))
class Meta: class Meta:
verbose_name = _("remarque sur matériel") verbose_name = _("defaut matériel")
verbose_name_plural = _("remarques sur le matériel") verbose_name_plural = _("défauts sur le matériel")
def __str__(self):
return "%s : %s" % (self.equipment.name,
self.remark)
class EquipmentLost(models.Model):
lost_at = models.DateField(
_("perdu le"),
default=date.today,
)
equipment = models.ForeignKey(
Equipment,
verbose_name=_("matériel"),
on_delete=models.CASCADE,
related_name="losts",
help_text=_("Matériel concerné par la perte"),
)
ids = IdField()
class EquipmentRevision(models.Model):
date = models.DateField(
_("date"),
default=date.today,
)
equipment = models.ForeignKey(
Equipment,
verbose_name=_("matériel"),
on_delete=models.CASCADE,
related_name="revisions",
help_text=_("Matériel concerné par les révisions"),
)
remark = models.TextField(_("remarque sur la révision"))
ids = IdField()
class Meta:
verbose_name = _("révision de matériel")
verbose_name_plural = _("révisions de matériel")
def __str__(self): def __str__(self):
return "%s : %s" % (self.equipment.name, return "%s : %s" % (self.equipment.name,

View file

@ -0,0 +1,12 @@
.nice_select input[type="checkbox"] {
display: none;
}
.nice_select input[type="checkbox"]:checked + label {
background: red;
color:white;
}
.nice_select ul {
display: inline-block;
}

109
equipment/tables.py Normal file
View file

@ -0,0 +1,109 @@
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import Group
from django.db.models import Q
from django.http.request import QueryDict
import django_filters
from django_filters.widgets import LinkWidget
from django_tables2.utils import A
import django_tables2 as tables
from .models import Equipment, EquipmentCategory
class EquipmentFilter(django_filters.FilterSet):
owner = django_filters.ModelChoiceFilter(
field_name='owner',
queryset=Group.objects.all(),
widget=LinkWidget(),
)
category = django_filters.ModelChoiceFilter(
field_name='category',
queryset=EquipmentCategory.objects.all(),
widget=LinkWidget(),
method='filter_category',
)
def filter_category(self, queryset, name, value):
return queryset.in_category(value)
def get_categories(self, qs):
"""
rend les catégories qui servent à filtrer les Equipments de qs
ie les catégories des equipments et tous leurs parents
"""
filtre = Q(id__lt=0)
for eq in qs:
current = eq.category
for k in range(100):
if current is None:
break
filtre |= Q(id=current.id)
current = current.parent
return EquipmentCategory.objects.filter(filtre)
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
# On que les requêtes vides rendent quelque chose
if data is None:
data = QueryDict('category&owner')
super().__init__(data=data, queryset=queryset,
request=request, prefix=prefix)
if self.queryset is not None:
for filter_ in self.filters.values():
if filter_.queryset.model == Group:
own_ids = [eq.owner.id for eq in self.queryset
if eq.owner is not None]
filtre = Q(id__lt=0)
for own_id in own_ids:
filtre |= Q(id=own_id)
filter_.queryset = Group.objects.filter(filtre)
if filter_.queryset.model == EquipmentCategory:
filter_.queryset = self.get_categories(self.queryset)
class Meta:
model = Equipment
fields = ['category', 'owner']
class AbstractEquipmentTable(tables.Table):
stock_aviable_p = tables.Column(
accessor=A('stock_aviable_p'),
orderable=False, # TODO le rendre ordorable
verbose_name=_("Quantité disponible"),
)
full_category_p = tables.Column(
accessor=A('full_category_p'),
order_by=('category'),
verbose_name=_("Catégorie"),
)
name = tables.LinkColumn(
'equipment:detail',
args=[A('pk')],
verbose_name=_("Matériel"),
)
admin = tables.LinkColumn(
'admin:equipment_equipment_change',
attrs={
'a': {'class': 'glyphicon glyphicon-cog'}
},
text="",
orderable=False,
args=[A('pk')],
verbose_name=_(""),
)
def before_render(self, request):
if (request.user.is_staff and
request.user.has_perm('equipment_change_equipment')):
self.columns.show('admin')
else:
self.columns.hide('admin')
class EquipmentTable(AbstractEquipmentTable):
class Meta:
model = Equipment
template_name = 'equipment/tables/bootstrap-responsive.html'
fields = ['name', 'stock', 'owner', ]
sequence = ['admin', 'name', 'stock', 'stock_aviable_p',
'full_category_p', 'owner', ]

View file

@ -0,0 +1,13 @@
{% extends "shared/base.html" %}
{% load i18n staticfiles %}
{% block title %}{% trans "Matériel" %}{% endblock %}
{% block content %}
<h2>{{ equipment.name }}<a class="pull-right glyphicon glyphicon-cog" href="{% url 'admin:equipment_equipment_change' equipment.id %}"></a></h2>
{% endblock %}
{% block aside %}
Coucou :)
{% endblock %}

View file

@ -0,0 +1,50 @@
{% extends "shared/base.html" %}
{% load i18n staticfiles %}
{% block title %}{% trans "Matériel" %}{% endblock %}
{% block content %}
<h1 class="equipment">{% trans "Inventaire" %}</h1>
<div class="module-list">
<a href="{% url 'equipment:list' %}" class="module equipment">
<span class="glyphicon glyphicon-list-alt"></span>
{% trans "Tout le matériel" %}
</a>
<a href="#TODO" class="module equipment">
<span class="glyphicon glyphicon-list-alt"></span>
{% trans "Disponible" %}
</a>
</div>
<h2 class="staff">{% trans "Liste par Propriétaire" %}</h2>
<div class="module-list">
{% for owner in owners %}
<a href="{% url 'equipment:list_by_owner' owner.id %}" class="module staff">
<span class="glyphicon glyphicon-user"></span>
{{ owner.name }}
</a>
{% endfor %}
</div>
<h2 class="equipment">{% trans "Liste par Catégorie" %}</h2>
<div class="tree">
<ul>
{% for node in root_cat %}
{% include "equipment/tree_cat.html" %}
{% endfor %}
</ul>
</div>
{% endblock %}
{% block aside %}
<div class="heading">
{{ nb_type }} <span class="sub">{% trans "référence" %}{{ nb_type|pluralize}}</span>
</div>
<div class="heading separator">
{{ stock }} <span class="sub">{% trans "item" %}{{ stock|pluralize}}</span>
</div>
<div class="heading inverted small">
{{ categories.count }} <span class="sub">{% trans "catégorie" %}{{ categories.count|pluralize}}</span>
</div>
<div class="heading inverted small ">
{{ owners.count }} <span class="sub">{% trans "propriéaire" %}{{ owners.count|pluralize}}</span>
</div>
{% endblock %}

View file

@ -0,0 +1,30 @@
{% extends "shared/base.html" %}
{% load render_table from django_tables2 %}
{% load bootstrap3 %}
{% load i18n staticfiles %}
{% block title %}{% trans "Matériel" %}{% endblock %}
{% block content %}
<h1 class="equipment">Inventaire</h1>
{% if subtitle %}
<h2 class="equipment">{{ subtitle }}</h2>
{% endif %}
{% render_table table %}
{% endblock %}
{% block aside %}
<div class="heading">
{{ nb_type }} <span class="sub">{% trans "référence" %}{{ nb_type|pluralize}}</span>
</div>
<div class="heading separator">
{{ stock }} <span class="sub">{% trans "item" %}{{ stock|pluralize}}</span>
</div>
{% if filter %}
<div class="text inverted">
<form id="filter_form" action="" method="get" class="form form-inline">
{% bootstrap_form filter.form layout='horizontal' size='lg' %}
</form>
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends 'equipment/tables/bootstrap.html' %}
{% block table-wrapper %}
<div class="table-container table-responsive">
{% block table %}
{{ block.super }}
{% endblock table %}
{% if table.page and table.paginator.num_pages > 1 %}
{% block pagination %}
{{ block.super }}
{% endblock pagination %}
{% endif %}
</div>
{% endblock table-wrapper %}

View file

@ -0,0 +1,103 @@
{% load django_tables2 %}
{% load i18n %}
{% block table-wrapper %}
<div class="table-container">
{% block table %}
<table {% render_attrs table.attrs class="table table-striped" %}>
{% block table.thead %}
{% if table.show_header %}
<thead>
<tr>
{% for column in table.columns %}
<th {{ column.attrs.th.as_html }}>
{% if column.orderable %}
<a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a>
{% else %}
{{ column.header }}
{% endif %}
</th>
{% endfor %}
</tr>
</thead>
{% endif %}
{% endblock table.thead %}
{% block table.tbody %}
<tbody>
{% for row in table.paginated_rows %}
{% block table.tbody.row %}
<tr {{ row.attrs.as_html }}>
{% for column, cell in row.items %}
<td {{ column.attrs.td.as_html }}>{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}</td>
{% endfor %}
</tr>
{% endblock table.tbody.row %}
{% empty %}
{% if table.empty_text %}
{% block table.tbody.empty_text %}
<tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
{% endblock table.tbody.empty_text %}
{% endif %}
{% endfor %}
</tbody>
{% endblock table.tbody %}
{% block table.tfoot %}
{% if table.has_footer %}
<tfoot>
<tr>
{% for column in table.columns %}
<td {{ column.attrs.tf.as_html }}>{{ column.footer }}</td>
{% endfor %}
</tr>
</tfoot>
{% endif %}
{% endblock table.tfoot %}
</table>
{% endblock table %}
{% if table.page and table.paginator.num_pages > 1 %}
{% block pagination %}
<nav aria-label="Table navigation">
<ul class="pagination">
{% if table.page.has_previous %}
{% block pagination.previous %}
<li class="previous">
<a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">
<span aria-hidden="true">&laquo;</span>
{% trans 'previous' %}
</a>
</li>
{% endblock pagination.previous %}
{% endif %}
{% if table.page.has_previous or table.page.has_next %}
{% block pagination.range %}
{% for p in table.page|table_page_range:table.paginator %}
<li {% if p == table.page.number %}class="active"{% endif %}>
{% if p == '...' %}
<a href="#">{{ p }}</a>
{% else %}
<a href="{% querystring table.prefixed_page_field=p %}">
{{ p }}
</a>
{% endif %}
</li>
{% endfor %}
{% endblock pagination.range %}
{% endif %}
{% if table.page.has_next %}
{% block pagination.next %}
<li class="next">
<a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">
{% trans 'next' %}
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endblock pagination.next %}
{% endif %}
</ul>
</nav>
{% endblock pagination %}
{% endif %}
</div>
{% endblock table-wrapper %}

View file

@ -0,0 +1,11 @@
<li> <a href="{% url "equipment:list_by_category" node.id %}"><span class="category_node">{{node.name}}</span></a>
{%if node.children %}
<ul>
{%for ch in node.children.all %}
{%with node=ch template_name="equipment/tree_cat.html" %}
{%include template_name%}
{%endwith%}
{%endfor%}
</ul>
{%endif%}
</li>

View file

@ -0,0 +1 @@
{% include "django/forms/widgets/input.html" %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{{ widget.label }}</label>

View file

@ -0,0 +1,5 @@
{% with id=widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}{% if widget.attrs.class %} class="{{ widget.attrs.class }} nice_select"{% endif %} style="display: inline-block">{% for group, options, index in widget.optgroups %}{% if group %}
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}"{% endif %}>{% endif %}{% for option in options %}
<li style="list-style-type: none;">{% include option.template_name with widget=option %}</li>{% endfor %}{% if group %}
</ul></li>{% endif %}{% endfor %}
</ul>{% endwith %}

11
equipment/urls.py Normal file
View file

@ -0,0 +1,11 @@
from django.conf.urls import url
from .views import EquipmentList, EquipmentView, EquipmentListByCategory, EquipmentListByOwner, EquipmentHome
app_name = 'equipment'
urlpatterns = [
url(r'^$', EquipmentHome.as_view(), name='home'),
url(r'^all/$', EquipmentList.as_view(), name='list'),
url(r'^(?P<pk>[0-9]+)/$', EquipmentView.as_view(), name='detail'),
url(r'^c/(?P<pk>[0-9]+)/$', EquipmentListByCategory.as_view(), name='list_by_category'),
url(r'^o/(?P<pk>[0-9]+)/$', EquipmentListByOwner.as_view(), name='list_by_owner'),
]

97
equipment/views.py Normal file
View file

@ -0,0 +1,97 @@
from .models import Equipment, EquipmentCategory
from django.contrib.auth.models import Group
from django.db.models import Sum
from django.views.generic import DetailView, ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from django_filters.views import FilterView
from django_tables2.views import SingleTableMixin
from .tables import EquipmentTable, EquipmentFilter
class EquipmentHome(LoginRequiredMixin, ListView):
template_name = 'equipment/home.html'
context_object_name = 'categories'
model = EquipmentCategory
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# TODO remplacer par les vrais owners
context['owners'] = Group.objects.all()
categories = (EquipmentCategory.objects.order_by('name')
.prefetch_related('children'))
context['root_cat'] = categories.filter(parent=None)
queryset = Equipment.objects.all()
context['stock'] = queryset.aggregate(Sum('stock'))['stock__sum']
context['nb_type'] = queryset.count()
return context
class EquipmentListAbstract(LoginRequiredMixin, SingleTableMixin,FilterView):
table_class = EquipmentTable
filterset_class = EquipmentFilter
template_name = 'equipment/list.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['stock'] = self.queryset.aggregate(Sum('stock'))['stock__sum']
context['nb_type'] = self.queryset.count()
return context
class EquipmentList(EquipmentListAbstract):
def get_queryset(self):
self.queryset = Equipment.objects.all()
return self.queryset
class EquipmentListByCategory(EquipmentListAbstract):
def get_category(self):
try:
pk = self.kwargs.get('pk')
except KeyError:
raise AttributeError(
"View %s must be called with an object "
"pk in the URLconf." % self.__class__.__name__
)
return EquipmentCategory.objects.get(id=pk)
def get_queryset(self):
cat = self.get_category()
self.queryset = Equipment.objects.all().in_category(cat)
return self.queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
cat = self.get_category()
context['subtitle'] = "Dans {cat}".format(cat=cat.full_name())
return context
class EquipmentListByOwner(EquipmentListAbstract):
def get_owner(self):
try:
pk = self.kwargs.get('pk')
except KeyError:
raise AttributeError(
"View %s must be called with an object "
"pk in the URLconf." % self.__class__.__name__
)
return Group.objects.get(id=pk)
def get_queryset(self):
owner = self.get_owner()
self.queryset = Equipment.objects.filter(owner=owner)
return self.queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
owner = self.get_owner()
context['subtitle'] = "Matériel de {owner}".format(owner=owner)
return context
class EquipmentView(LoginRequiredMixin, DetailView):
model = Equipment
template_name = 'equipment/detail.html'

View file

@ -0,0 +1 @@
default_app_config = 'event.apps.EventConfig'

View file

@ -1,3 +1,97 @@
from django.contrib import admin from django.contrib import admin
# Register your models here. from .models import Event, Place, ActivityTag, Activity, ActivityTemplate # TODO add me
from equipment.models import EquipmentAttribution
class EquipmentAttributionExtraInline(admin.TabularInline):
autocomplete_fields = ['equipment', ]
model = EquipmentAttribution
extra = 0
classes = ['collapse']
class EventAdmin(admin.ModelAdmin):
list_display = ['title', 'slug', 'beginning_date', 'ending_date']
readonly_fields = ['created_by', 'created_at', ]
ordering = ['title', 'beginning_date', 'ending_date', ]
search_fields = ['title', 'decription', ]
list_filter = ['beginning_date', 'ending_date', ]
date_hierarchy = 'beginning_date'
class PlaceAdmin(admin.ModelAdmin):
list_display = ['name', 'event', ]
ordering = ['name', 'event', ]
search_fields = ['name', ]
list_filter = ['event', ]
class ActivityTagAdmin(admin.ModelAdmin):
list_display = ['name', 'event', 'is_public', ]
ordering = ['name', 'event', 'is_public', ]
search_fields = ['name', ]
list_filter = ['event', 'is_public', ]
class ActivityTemplateAdmin(admin.ModelAdmin):
save_as_continue = True
save_on_top = True
list_display = ['name', 'title', 'event', 'is_public', ]
ordering = ['name', 'title', 'event', 'has_perm', ]
search_fields = ['name', 'title', 'description', 'remark', ]
list_filter = ['event', 'is_public', 'has_perm', 'tags', ]
filter_horizontal = ['tags', 'places', ]
fieldsets = (
('Identifiant', {
'fields': ('name', ),
}),
('Général', {
'fields': ('event', 'title', 'is_public', 'places', ),
'description': "Tous ces champs sont héritables (Sauf Évènement)",
}),
('Permanences', {
'fields': ('has_perm', ('min_perm', 'max_perm', ), ),
'classes': ('collapse',),
'description': "Tous ces champs sont héritables",
}),
('Descriptions', {
'fields': ('description', 'tags', 'remarks', ),
'classes': ('collapse',),
'description': "Tous ces champs sont héritables",
}),
)
class ActivityAdmin(admin.ModelAdmin):
save_as = True
save_on_top = True
list_display = ['title', 'event', 'is_public', 'parent', ]
ordering = ['title', 'event', 'has_perm', 'parent', ]
search_fields = ['title', 'description', 'remark', ]
list_filter = ['event', 'is_public', 'has_perm', 'tags', ]
filter_horizontal = ['tags', 'places', 'staff', ]
inlines = [EquipmentAttributionExtraInline, ]
fieldsets = (
('Général', {
'fields': ('event', 'parent', 'title', 'is_public', 'beginning', 'end', 'places', ),
'description': "Tous ces champs sont héritables (sauf parent et Évènement)",
}),
('Permanences', {
'fields': ('has_perm', ('min_perm', 'max_perm', ), 'staff', ),
'classes': ('wide',),
'description': "Tous ces champs sont héritables (sauf les gens en perm)",
}),
('Descriptions', {
'fields': ('description', 'tags', 'remarks', ),
'classes': ('collapse',),
'description': "Tous ces champs sont héritables",
}),
)
admin.site.register(Event, EventAdmin)
admin.site.register(Place, PlaceAdmin)
admin.site.register(ActivityTag, ActivityTagAdmin)
admin.site.register(ActivityTemplate, ActivityTemplateAdmin)
admin.site.register(Activity, ActivityAdmin)

View file

@ -1,5 +1,7 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class EventConfig(AppConfig): class EventConfig(AppConfig):
name = 'event' name = 'event'
verbose_name = _("Évènement")

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-12 12:47 # Generated by Django 1.11.15 on 2018-08-06 07:51
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
@ -74,8 +74,8 @@ class Migration(migrations.Migration):
('slug', models.SlugField(help_text="Seulement des lettres, des chiffres ou les caractères '_' ou '-'.", unique=True, verbose_name='identificateur')), ('slug', models.SlugField(help_text="Seulement des lettres, des chiffres ou les caractères '_' ou '-'.", unique=True, verbose_name='identificateur')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='date de création')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='date de création')),
('description', models.TextField(verbose_name='description')), ('description', models.TextField(verbose_name='description')),
('beginning_date', models.DateTimeField(verbose_name='date de début')), ('beginning_date', models.DateTimeField(help_text="date publique de l'évènement", verbose_name='date de début')),
('ending_date', models.DateTimeField(verbose_name='date de fin')), ('ending_date', models.DateTimeField(help_text="date publique de l'évènement", verbose_name='date de fin')),
('created_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL, verbose_name='créé par')), ('created_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL, verbose_name='créé par')),
], ],
options={ options={

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-08-20 15:29
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('event', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='activitytemplate',
name='name',
field=models.CharField(default='change_me!', help_text='Ne sera pas affiché', max_length=200, verbose_name='Nom du template'),
preserve_default=False,
),
migrations.AlterField(
model_name='activity',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'),
),
migrations.AlterField(
model_name='activitytemplate',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='event.Event', verbose_name='évènement'),
),
]

View file

@ -1,5 +1,5 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import FieldDoesNotExist, FieldError from django.core.exceptions import FieldDoesNotExist, FieldError, ValidationError
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -123,7 +123,6 @@ class AbstractActivityTemplate(SubscriptionMixin, models.Model):
Event, Event,
verbose_name=_("évènement"), verbose_name=_("évènement"),
on_delete=models.CASCADE, on_delete=models.CASCADE,
editable=False,
) )
is_public = models.NullBooleanField( is_public = models.NullBooleanField(
_("est public"), _("est public"),
@ -167,12 +166,34 @@ class AbstractActivityTemplate(SubscriptionMixin, models.Model):
class ActivityTemplate(AbstractActivityTemplate): class ActivityTemplate(AbstractActivityTemplate):
name = models.CharField(
_("Nom du template"),
max_length=200,
help_text=_("Ne sera pas affiché"),
)
class Meta: class Meta:
verbose_name = _("template activité") verbose_name = _("template activité")
verbose_name_plural = _("templates activité") verbose_name_plural = _("templates activité")
def __str__(self): def __str__(self):
return self.title return self.name
def clean(self):
errors = []
# On clean les nombre de permanents
if not self.has_perm:
self.max_perm = None
self.min_perm = None
else:
if self.min_perm > self.max_perm:
errors.append(ValidationError(
_("Nombres de permanents incompatibles"),
code='wrong-nb-perm',
))
if errors != []:
raise ValidationError(errors)
class Activity(AbstractActivityTemplate): class Activity(AbstractActivityTemplate):
@ -193,6 +214,86 @@ class Activity(AbstractActivityTemplate):
beginning = models.DateTimeField(_("heure de début")) beginning = models.DateTimeField(_("heure de début"))
end = models.DateTimeField(_("heure de fin")) end = models.DateTimeField(_("heure de fin"))
def clean(self):
errors = []
# On clean les nombre de permanents
if not self.get_herited('has_perm'):
self.max_perm = None
self.min_perm = None
else:
if self.get_herited('min_perm') > self.get_herited('max_perm'):
errors.append(ValidationError(
_("Nombres de permanents incompatibles"),
code='wrong-nb-perm',
))
# On valide l'héritage
for f in self._meta.get_fields():
try:
# On réccupère le field du parent
attrname = f.name
tpl_field = ActivityTemplate._meta.get_field(attrname)
# Peut-être que ce n'est pas un field
# concerné par l'héritage
except FieldDoesNotExist:
continue
# Y'a certains champs dont on se moque
if attrname in ['id', 'staff', 'tags', ]:
continue
# C'est plus compliqué que ça pour les nb_perm
if attrname in ['max_perm', 'min_perm', ]:
if not self.get_herited('has_perm'):
continue
# On a un Many to Many, on lit différement
if tpl_field.many_to_many:
pass
# # On a pas spécifié
# if not value.exists():
# # On a pas de parent
# if self.parent is None:
# errors.append(ValidationError(
# _("N'hérite pas d'un template, spécifier le champs : %(attr)s"),
# code='bad-overriding',
# params={'attr': f.verbose_name},
# ))
# else:
# pvalue = getattr(self.parent, attrname)
# # On a un parent qui ne dit rien
# if not pvalue.exists():
# errors.append(ValidationError(
# _("Champs non précisé chez le parent, spécifier : %(attr)s"),
# code='bad-overriding',
# params={'attr': f.verbose_name},
# ))
else:
value = getattr(self, attrname)
# On a pas spécifié
if value is None:
# On a pas de parent
if self.parent is None:
errors.append(ValidationError(
_("N'hérite pas d'un template, spécifier le champs : %(attr)s"),
code='bad-overriding',
params={'attr': f.verbose_name},
))
else:
pvalue = getattr(self.parent, attrname)
# On a un parent qui ne dit rien
if pvalue is None:
errors.append(ValidationError(
_("Champs non précisé chez le parent, spécifier : %(attr)s"),
code='bad-overriding',
params={'attr': f.verbose_name},
))
if errors != []:
raise ValidationError(errors)
def get_herited(self, attrname): def get_herited(self, attrname):
try: try:
tpl_field = ActivityTemplate._meta.get_field(attrname) tpl_field = ActivityTemplate._meta.get_field(attrname)
@ -207,9 +308,9 @@ class Activity(AbstractActivityTemplate):
if tpl_field.many_to_many: if tpl_field.many_to_many:
if value.exists(): if value.exists():
return value return value
else: elif self.parent is not None:
return getattr(self.parent, attrname) return getattr(self.parent, attrname)
elif value is None: elif value is None and self.parent is not None:
return getattr(self.parent, attrname) return getattr(self.parent, attrname)
else: else:
return value return value

View file

@ -0,0 +1,373 @@
/* Calendar */
#cal-container {
width: 100%;
height: 100%;
padding: 0;
min-height: 80vh;
display: block;
position: relative;
line-height: 100%;
}
#cal-container,
#cal-container * {
box-sizing: border-box;
}
/* Time slots */
#cal-container .cal-time-slot-container {
display: grid;
grid-template-rows: 30px auto;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: calc(100% + 30px + 10px);
padding: 0;
z-index: 10;
}
#cal-container .cal-time-slot {
border-right: 1px solid #EEE;
background-color: #FAFAFA;
}
#cal-container .cal-time-slot:nth-child(even) {
background-color: #F4F4F4;
}
#cal-container .cal-time-slot:last-child {
border-right: 0;
}
#cal-container .cal-time-slot-hour {
padding: 0 0 0 calc(100% - 1.4rem + 7px);
background-color: #F9F9F9;/* #3D3D3D; */
border-bottom: 1px solid #AAA;
color: #333;
font-size: 1.4rem;
line-height: 30px;
vertical-align: middle;
}
#cal-container .cal-time-slot-hour:nth-child(even) {
background-color: #FCFCFC;/* #464646; */
}
#cal-container .cal-time-slot-hour:first-child {
color: transparent;
border-right: 0;
}
#cal-container .cal-time-slot-hour.cal-last-hour {
padding: 0;
color: transparent;
}
#cal-container .cal-time-slot.cal-last-hour,
#cal-container .cal-time-slot-hour.cal-last-hour {
border-right: 1px solid #AAA;
}
#cal-container .cal-time-slot.cal-first-hour,
#cal-container .cal-time-slot-hour.cal-first-hour {
border-left: 1px solid #AAA;
}
/* Events */
#cal-container .cal-event-container {
display: grid;
/* grid-template-columns: repeat(24, 1fr); */
/* grid-template-rows: repeat(12, auto); */
position: absolute;
top: 40px;
left: 0;
width: 100%;
height: 100%;
padding: 0;
z-index: 100;
}
#cal-container .cal-event {
position: relative;
height: 80px;
margin: 2px 0;
/* padding: 5px; */
/* background-color: #EFEFEF; */
border-radius: 3px;
/* border: 1px solid #CCC; */
border-width: 1px;
border-style: solid;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* z-index: 500; */
transition: 50ms ease-in;
}
#cal-container .cal-event:hover {
/* background-color: #FEDDDD; */
}
#cal-container .cal-event > * {
display: none;
margin: 5px;
}
#cal-container .cal-event > .cal-event-name,
#cal-container .cal-event > .cal-event-location,
#cal-container .cal-event > .cal-event-perm-count {
display: block;
}
#cal-container .cal-event > .cal-event-name {
font-weight: bold;
}
#cal-container .cal-event > .cal-event-location {
font-style: italic;
}
#cal-container .cal-event > .cal-event-perm-count {
position: absolute;
bottom: 0;
right: 0;
}
#cal-container .cal-event:not(.cal-event-subscribed) > .cal-event-perm-count.cal-perms-missing {
width: calc(100% - 10px);
right: auto;
margin: 5px;
padding: 5px;
background-color: #FFF;
border: 2px solid #E44;
color: #E44;
font-weight: bold;
border-radius: 3px;
overflow: hidden;
}
#cal-container .cal-event.cal-event-subscribed {
border-width: 3px;
border-color: #000;
}
#cal-container .cal-event.cal-event-subscribed::after {
content: "✔";
position: absolute;
left: 0;
bottom: 0;
width: 16px;
height: 16px;
padding: 1px;
color: #fff;
background-color: #000;
border-top-right-radius: 3px;
}
/* Event details popup */
#cal-container .cal-event-details {
position: absolute;
min-height: 100px;
/* min-width: 40%; */
max-width: 80%;
padding: 20px;
background-color: #333;
color: #FFF;
border-radius: 4px;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.6);
z-index: 1000;
}
#cal-container .cal-event-details:after {
bottom: 100%;
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-bottom-color: #333;
border-width: 20px;
margin-left: -20px;
}
#cal-container .cal-event-details.above-event:after {
top: 100%;
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-top-color: #333;
border-width: 20px;
margin-left: -20px;
}
#cal-container .cal-event-details * {
z-index: 1000;
}
#cal-container .cal-event-details .cal-detail-close-button {
width: 35px;
height: 35px;
position: absolute;
top: 10px;
right: 10px;
margin: 0;
padding: 5px;
background: transparent;
border: none;
border-radius: 50%;
font-size: 1.6rem;
color: #BBB;
transition: 100ms ease-out;
}
#cal-container .cal-event-details .cal-detail-close-button:hover {
background-color: #484848;
color: #EFEFEF;
}
#cal-container .cal-event-details a,
#cal-container .cal-event-details a:hover {
color: #FFF;
text-decoration: none;
}
#cal-container .cal-event-details .cal-detail-name > h3 {
margin: 0 0 25px 26px;
padding: 10px;
border-radius: 4px;
color: #FFF;
font-size: 2.5rem;
text-transform: uppercase;
text-align: center;
}
#cal-container .cal-event-details .cal-detail-name > h3::after {
content: "";
display: inline-block;
width: 16px;
height: 16px;
margin: 0 0 0 10px;
vertical-align: middle;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAA90lEQVQoz2WRsS6DURiGn/yaaNqBySpGicFg6iJcAYuuNBaJpJEg6gLcADFIdWR0BZZKS4SBRTe3YFf/Y/j/c3LUe4Zz3vd78+U734uUJ7Ptg7k68NDpoIdyU9V3TzzwRdXt1HCq9pwR510W616oZ8Gwoe6KWFN1ScStogui3sRJ9uxYLd/n6hTuaCy3XHfNWuR6hM+OojBQdSXyJ0cZizwSsMo3kEc+ZCEjxZgxE8j4oBFZhRww8gaff4fEL3UuGfK4uG5L4VLVfvrNCrDJHfd0gSrXQB2AJvu0+Cm8HbXnbGw9seqwWH2z65Wv/8MKcfdVHaZx/wKtOg5kifzQhwAAAABJRU5ErkJggg==);
opacity: 0.6;
}
#cal-container .cal-event-details .cal-detail-name:hover > h3 {
margin-right: 26px;
margin-bottom: 10px;
background-color: #484848;
color: #FFF;
}
#cal-container .cal-event-details .cal-detail-name:hover > h3::after {
content: "Cliquez pour afficher les détails.";
display: block;
width: auto;
height: 15px;
padding: 2px 0 0 0;
font-size: 1.2rem;
background-image: none;
color: #DDD;
}
#cal-container .cal-event-details .cal-detail-name:hover + .cal-detail-close-button {
opacity: 0;
}
#cal-container .cal-event-details table {
margin: 0 auto;
font-size: 1.8rem;
}
#cal-container .cal-event-details td.cal-detail-label {
padding: 0 10px 10px 0;
font-weight: bold;
text-align: right;
}
#cal-container .cal-event-details td.cal-detail-value {
padding: 0 0 10px 10px;
text-align: left;
}
#cal-container .cal-event-details .cal-detail-perm-area {
margin: 10px 0;
padding: 10px;
background-color: #DFDFDF;
color: #333;
text-align: center;
border-radius: 4px;
}
#cal-container .cal-event-details .cal-detail-perm-title {
display: block;
margin: 0 0 10px 0;
}
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-count {
margin: 0 10px 0 0;
font-size: 2.5rem;
vertical-align: middle;
}
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-count.cal-perms-missing {
color: #E44;
}
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-count.cal-perms-full {
color: #393;
}
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-subscription-switch {
margin: 0 0 0 10px;
padding: 10px;
font-size: 1.8rem;
vertical-align: middle;
}
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-nb-missing-perms {
margin: 20px 0 0 0;
padding: 5px;
background-color: #FFF;
border-radius: 4px;
text-align: center;
font-size: 1.5rem;
color: #E44;
font-weight: bold;
}
#cal-container .cal-event-details .cal-detail-description {
margin: 20px 0 20px 0;
color: #DDD;
font-size: 1.8rem;
font-style: italic;
text-align: justify;
line-height: 120%;
}
#cal-container .cal-event-details .cal-detail-tag {
display: inline-block;
margin: 5px;
padding: 5px;
border: 1px solid #DDD;
}

108
event/static/css/tipso.css Normal file
View file

@ -0,0 +1,108 @@
/* Tipso Bubble Styles */
.tipso_bubble, .tipso_bubble > .tipso_arrow{
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.tipso_bubble {
position: absolute;
text-align: center;
border-radius: 6px;
z-index: 9999;
}
.tipso_style{
/* cursor: help; */
border-bottom: 1px dotted;
}
.tipso_title {
padding: 3px 0;
border-radius: 6px 6px 0 0;
font-weight: 700;
}
.tipso_content {
word-wrap: break-word;
padding: 0.5em;
}
/* Tipso Bubble size classes - Similar to Foundation's syntax*/
.tipso_bubble.tiny {
font-size: 0.6rem;
}
.tipso_bubble.small {
font-size: 0.8rem;
}
.tipso_bubble.default {
font-size: 1rem;
}
.tipso_bubble.large {
font-size: 1.2rem;
width: 100%;
}
.tipso_bubble.cal_small {
font-size: 1.6rem;
}
/* Tipso Bubble Div */
.tipso_bubble > .tipso_arrow{
position: absolute;
width: 0; height: 0;
border: 8px solid;
pointer-events: none;
}
.tipso_bubble.top > .tipso_arrow {
border-top-color: #000;
border-right-color: transparent;
border-left-color: transparent;
border-bottom-color: transparent;
top: 100%;
left: 50%;
margin-left: -8px;
}
.tipso_bubble.bottom > .tipso_arrow {
border-bottom-color: #000;
border-right-color: transparent;
border-left-color: transparent;
border-top-color: transparent;
bottom: 100%;
left: 50%;
margin-left: -8px;
}
.tipso_bubble.left > .tipso_arrow {
border-left-color: #000;
border-top-color: transparent;
border-bottom-color: transparent;
border-right-color: transparent;
top: 50%;
left: 100%;
margin-top: -8px;
}
.tipso_bubble.right > .tipso_arrow {
border-right-color: #000;
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
top: 50%;
right: 100%;
margin-top: -8px;
}
.tipso_bubble .top_right_corner,
.tipso_bubble.top_right_corner {
border-bottom-left-radius: 0;
}
.tipso_bubble .bottom_right_corner,
.tipso_bubble.bottom_right_corner {
border-top-left-radius: 0;
}
.tipso_bubble .top_left_corner,
.tipso_bubble.top_left_corner {
border-bottom-right-radius: 0;
}
.tipso_bubble .bottom_left_corner,
.tipso_bubble.bottom_left_corner {
border-top-right-radius: 0;
}

1184
event/static/js/calendar.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
$(function(){
function initEnrolment(elt) {
elt = $(elt);
elt.find("form.enrolment").on("submit", function() {
elt.addClass("sending-request");
var form = this;
var url = form.action + "?ajax";
$.post(url, $(form).serialize(), function(data) {
elt.html(data);
elt.removeClass("sending-request");
initEnrolment(elt);
});
return false;
});
}
$.each($(".activity-summary"), function(i, item) { initEnrolment(item) });
});

View file

@ -0,0 +1,84 @@
// Interval graph coloring algorithm, by Twal
class IntervalColoration {
constructor (intervals) {
this.intervals = intervals;
this.n = this.intervals.length;
this.computeInterferenceGraph();
this.computePEO();
this.computeColoration();
}
computeInterferenceGraph() {
this.adj = new Array(this.n);
for (let i = 0; i < this.n; ++i) {
this.adj[i] = [];
}
for (let i = 0; i < this.n; ++i) {
for (let j = 0; j < i; ++j) {
let inti = this.intervals[i];
let intj = this.intervals[j];
if (inti[0] < intj[1] && intj[0] < inti[1]) {
this.adj[i].push(j);
this.adj[j].push(i);
}
}
}
}
//Perfect elimination order using Maximum Cardinality Search
//Runs in O(n^2), could be optimized in O(n log n)
computePEO() {
let marked = new Array(this.n);
let nbMarkedNeighbor = new Array(this.n);
this.perm = new Array(this.n);
for (let i = 0; i < this.n; ++i) {
marked[i] = false;
nbMarkedNeighbor[i] = 0;
}
for (let k = this.n-1; k >= 0; --k) {
let maxi = -1;
for (let i = 0; i < this.n; ++i) {
if (!marked[i] && (maxi == -1 || nbMarkedNeighbor[i] >= nbMarkedNeighbor[maxi])) {
maxi = i;
}
}
for (let i = 0; i < this.adj[maxi].length; ++i) {
nbMarkedNeighbor[this.adj[maxi][i]] += 1;
}
this.perm[maxi] = k;
marked[maxi] = true;
}
// console.log(this.perm);
}
computeColoration() {
this.colors = new Array(this.n);
let isColorUsed = new Array(this.n);
for (let i = 0; i < this.n; ++i) {
this.colors[i] = -1;
isColorUsed[i] = false;
}
for (let i = 0; i < this.n; ++i) {
let ind = this.perm[i];
for (let j = 0; j < this.adj[ind].length; ++j) {
let neigh = this.adj[ind][j];
if (this.colors[neigh] >= 0) {
isColorUsed[this.colors[neigh]] = true;
}
}
for (let j = 0; j < this.n; ++j) {
if (!isColorUsed[j]) {
this.colors[ind] = j;
break;
}
}
for (let j = 0; j < this.adj[ind].length; ++j) {
let neigh = this.adj[ind][j];
if (this.colors[neigh] >= 0) {
isColorUsed[this.colors[neigh]] = false;
}
}
}
}
}

1
event/static/js/tipso.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,52 @@
{% extends "shared/base.html" %}
{% load i18n staticfiles event_tags %}
{% load render_table from django_tables2 %}
{% block title %}{% trans "Activity" %}{% endblock %}
{% block content %}
<h1>{{ activity.title}}
{% if user.is_staff %}
<a class='glyphicon glyphicon-cog pull-right' href='{% url "admin:event_activity_change" activity.id %}'></a>
{% endif %}
</h1>
{% include "event/activity_summary.html" with activity=activity %}
<h2>Description</h2>
<p><strong>Description</strong>{{activity.description|default:"&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;"}}</p>
<p><strong>Remarque (staff)</strong>{{activity.remark|default:"&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;"}}</p>
<button class="collapsible active"><h3>Matériel</h3></button>
<div class="content fluid">
<table class="table table-responsive table-striped">
<tr>
<th>Matériel</th>
<th>Quantité</th>
<th>Propriétaire</th>
<th>Remarque</th>
</tr>
{% for att in attributions %}
<tr>
<td><a href="{% url 'equipment:detail' att.equipment.id %}">
{{ att.equipment }}</a></td>
<td>{{ att.amount }}</td>
<td>{{ att.equipment.owner }}</td>
<td>{{ att.remark }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
{% block aside %}
<div class="text">
Du
<strong>
{{ activity.beginning | date:"l d F Y H:i" }}
</strong>
au
<strong>
{{ activity.end | date:"l d F Y H:i" }}
</strong>
</p>
</div>
{% endblock %}

View file

@ -0,0 +1,79 @@
{% load i18n event_tags %}
{% with activity|get_herited:'has_perm' as has_perm %}
<table class="table table-responsive table-striped">
<tr>
<td>
<span class="glyphicon glyphicon-tree-deciduous"></span>
{% with activity|get_herited:'places' as places %}
{% if places.all %}
<span>{{ places.all |join:", "}}</span>
{% else %}
<span>&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;</span>
{% endif %}
{% endwith %}
</td>
<td>
<strong>public</strong>
&nbsp;:&nbsp;
{% with activity|get_herited:'is_public' as is_public %}
<span class="glyphicon {{ is_public|yesno:"yes glyphicon-ok-sign, no glyphicon-remove-sign, dunno glyphicon-question-sign"}}"></span>
{% endwith %}
</td>
<tr>
</tr>
<td>
<span class="glyphicon glyphicon-duplicate"></span>
{% if activity.parent %}
<span>{{ activity.parent}}</span>
{% else %}
<span>&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;</span>
{% endif %}
</td>
<td>
<strong>perm</strong>
&nbsp;:&nbsp;
<span class="glyphicon {{ has_perm|yesno:"yes glyphicon-ok-sign, no glyphicon-remove-sign, dunno glyphicon-question-sign"}}"></span>
</td>
<tr>
</tr>
<td>
<span class="glyphicon glyphicon-tag"></span>
{% with activity|get_herited:'tags' as tags %}
{% if tags.all %}
<span>{{ tags.all |join:", "}}</span>
{% else %}
<span>&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;</span>
{% endif %}
{% endwith %}
</td>
<td>
{% if has_perm %}
{{ activity|get_herited:'min_perm' }}
&le;
<strong>{{ activity.staff.count }}</strong>
&le;
{{ activity|get_herited:'max_perm' }}
{% endif %}
</td>
</tr>
</table>
{% if has_perm %}
<div class="container-fluid">
<div class="row">
<div class="col-sm-8">
<strong>En perm : </strong>
{% with activity.staff.all as staff %}
{% if staff %}
<span>{{ staff |join:", "}}</span>
{% else %}
<span>&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;</span>
{% endif %}
{% endwith %}
</div>
<div class="col-sm-4">
{% enrol_btn activity request.user %}
</div>
</div>
</div>
{% endif %}
{% endwith %}

View file

@ -1,25 +1,2 @@
{% extends "base.html" %} {% extends "shared/base.html" %}
{% block sidenav %}
<div class="centered">
<h5 class="centered banner-text">La Nuit 2017</h5>
</div>
<li>
<a href="index.html">
<i class="fa fa-dashboard"></i>
<span>Looool</span>
</a>
</li>
<li class="sub-menu">
<a href="javascript:;" >
<i class="fa fa-desktop"></i>
<span>Prout</span>
</a>
<ul class="sub">
<li><a href="general.html">Lolilol</a></li>
<li><a href="buttons.html">Lorem</a></li>
<li><a href="panels.html">Ipsum</a></li>
</ul>
</li>
{% endblock %}

View file

@ -0,0 +1,116 @@
{% extends "shared/fluid.html" %}
{% load i18n staticfiles event_tags %}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static "css/tipso.css" %}">
<link rel="stylesheet" href="{% static "css/calendar.css" %}">
<style>
#cal-toggle-unsubscribed-events-display {
display: block;
position: fixed;
bottom: 30px;
left: 30px;
border-bottom: 2px solid rgb(150, 50, 50);
font-size: 1.6rem;
font-weight: bold;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4),
0 1px 1px rgba(150, 50, 50, 0.7);
text-shadow: 0 0 7px rgb(150, 50, 50);
z-index: 5000;
}
</style>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script type="text/javascript" src="{% static "js/tipso.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/interval_coloration.js" %}"></script>
<script type="text/javascript" src="{% static "js/calendar.js" %}"></script>
<script type="text/javascript">
$(document).ready(() => {
let calendar = new Calendar({
startDate: new Date(2018, 10, 30, 8),
endDate: new Date(2018, 11, 2, 6),
eventDetailURLFormat: "https://cof.ens.fr/poulpe/event/activity/999999",
subscriptionURLFormat: "{% url "event:enrol_activity" 999999 %}?ajax=json",
csrfToken: $(".planning [name=csrfmiddlewaretoken]").val(),
groupEventsByLocation: true
});
// TODO: move this elsewhere
// Button to switch between:
// - displaying all events (default);
// - only displaying events for which the current user is enroled.
// Create the button
let toggleUnsubscribedEventDisplayButton = $("<button>")
.attr("type", "button")
.attr("id", "cal-toggle-unsubscribed-events-display")
.addClass("btn btn-primary")
.appendTo(calendar.containerNode);
// Set/update its label
function updateToggleButtonLabel () {
if (calendar.onlyDisplaySubscribedEvents) {
toggleUnsubscribedEventDisplayButton.html("Afficher toutes les activités");
}
else {
toggleUnsubscribedEventDisplayButton.html("Afficher seulement mes permanences");
}
}
updateToggleButtonLabel();
// Switch between display modes on click
toggleUnsubscribedEventDisplayButton.on("click", () => {
calendar.toggleEventsNotSubscribedByUser();
updateToggleButtonLabel();
});
// DEBUG: js console helpers, to be removed
console.log(calendar);
window["cal"] = calendar;
});
</script>
{% endblock %}
{% block content %}
<div class="planning">
{% csrf_token %}
{% regroup activities by beginning|date:"Y-m-d" as days_list %}
<div class="content fluid" id="cal-container">
{% for day in days_list %}
{% for activity in day.list %}
<div class="cal-event">
<span class="cal-event-id">{{ activity.id }}</span>
<span class="cal-event-name">{{ activity|get_herited:'title' }}</span>
<span class="cal-event-start-date">{{ activity.beginning | date:"j/m/Y H:i" }}</span>
<span class="cal-event-end-date">{{ activity.end | date:"j/m/Y H:i" }}</span>
{% with activity|get_herited:'places' as places %}
<span class="cal-event-location">{{ places.all | join:", " }}</span>
{% endwith %}
<span class="cal-event-description">{{ activity.description }}</span>
<span class="cal-event-url">{% url "event:activity" activity.id %}</span>
{% if activity|get_herited:'has_perm' %}
<span class="cal-event-has-perms">1</span>
<span class="cal-event-min-nb-perms">{{ activity|get_herited:'min_perm' }}</span>
<span class="cal-event-max-nb-perms">{{ activity|get_herited:'max_perm' }}</span>
<span class="cal-event-nb-perms">{{ activity.staff.count }}</span>
<span class="cal-event-subscribed">{% is_enrolled activity request.user %}</span>
{% endif %}
{% with activity|get_herited:'tags' as tags %}
{% for tag in tags.all %}
<span class="cal-event-tag">{{ tag.name }}</span>
{% endfor %}
{% endwith %}
</div>
{% endfor %}
{% endfor %}
</div>
{% endblock %}

View file

@ -0,0 +1,101 @@
{% extends "shared/base.html" %}
{% load i18n staticfiles event_tags %}
{% block title %}{% trans "Évènement" %}{% endblock %}
{% block extra_js %}
{{ block.super }}
<script type="text/javascript" src="{% static "js/enrol_event.js" %}"></script>
{% endblock %}
{% block content %}
<h1>{{ event.title}}
{% if perms.event.event_can_change and user.is_staff %}
<a class='glyphicon glyphicon-cog pull-right' href='{% url "admin:event_event_change" event.id %}'></a>
{% endif %}
</h1>
<p>{{ event.description }}</p>
<h2>Boîte à outils</h2>
<div class="module-list">
<a href="#TODO" class="module">
<span class="glyphicon glyphicon-duplicate"></span>
Templates d'activité
</a>
<a href="#TODO" class="module">
<span class="glyphicon glyphicon-tag"></span>
Tags spécifiques
</a>
<a href="#todo" class="module">
<span class="glyphicon glyphicon-tree-deciduous"></span>
lieux spécifiques
</a>
<a href="{% url "event:calendar" event.slug %}" class="module">
Calendrier
</a>
{% if staffuser %}
<a href="{% url "event:event" event.slug %}" class="module">
Toutes les activités
</a>
{% else %}
<a href="{% url "event:event-staff" event.slug user.username %}" class="module">
Mes perms
</a>
{% endif %}
</div>
{% if staffuser %}
<h2>Perms de {{ staffuser.first_name }} {{ staffuser.last_name }}</h2>
{% else %}
<h2>Planning</h2>
{% endif %}
<div class="planning">
{% regroup activities by beginning|date:"Y-m-d" as days_list %}
{% for day in days_list %}
{% with day.list|first as f_act %}
<button class="collapsible active"><h3>{{ f_act.beginning|date:"l d F" }}</h3></button>
<div class="content fluid">
{% endwith %}
{% for activity in day.list %}
<div class="{% cycle "" "inverted" %} activity">
<div class="activity-title">
<h4>
{% if perms.event.activity_can_change and user.is_staff %}
<a class='glyphicon glyphicon-cog' href='{% url "admin:event_activity_change" activity.id %}'></a>
{% endif %}
<a href="{% url "event:activity" activity.id %}">
{{ activity|get_herited:'title' }}
</a>
</h4>
<span class="pull-right">
de <strong>{{ activity.beginning | time:"H:i" }}</strong>
à <strong>{{ activity.end| time:"H:i" }}</strong>
</span>
<div class="activity-summary">
{% include "event/activity_summary.html" with activity=activity %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endblock %}
{% block aside %}
<div class="heading separator">
{{ activities.count }} <span class="sub">activité{{ activities.count|pluralize }}</span>
</div>
<div class="text inverted">
<p>Créé le {{ event.created_at | date:"l d F Y à H:i" }} par {{ event.created_by }}</p>
<p>
Du
<strong>
{{ event.beginning_date | date:"l d F Y H:i" }}
</strong>
au
<strong>
{{ event.ending_date | date:"l d F Y H:i" }}
</strong>
</p>
</div>
{% endblock %}

View file

@ -0,0 +1,6 @@
{% load i18n %}
<form method="POST" action="{% url "event:enrol_activity" activity.pk %}" class="enrolment {{ enrolled|yesno:"enrolled,unenrolled" }}">
{% csrf_token %}
<input type="hidden" name="goal" value="{{ enrolled|yesno:"unenrol,enrol" }}" />
{{ enrolled|yesno:_("Inscrit,") }} <input type="submit" value="{{ enrolled|yesno:_("Se désinscrire,S'inscrire") }}" class="btn btn-warning"/>
</form>

View file

@ -0,0 +1,21 @@
from django import template
register = template.Library()
@register.filter()
def get_herited(activity, attrname):
return activity.get_herited(attrname)
@register.inclusion_tag("event/tags/enrol_btn.html")
def enrol_btn(activity, user):
return {
"enrolled": activity.staff.filter(id=user.id).exists(),
"activity": activity,
}
@register.simple_tag
def is_enrolled(activity, user):
user_is_enrolled = activity.staff.filter(id=user.id).exists()
return "1" if user_is_enrolled else "0"

View file

@ -1,7 +1,14 @@
from django.conf.urls import url from django.conf.urls import url
from event.views import Index from event.views import Index, EventView, EventViewStaff, ActivityView, EnrolActivityView, EventCalendar
app_name = 'event' app_name = 'event'
urlpatterns = [ urlpatterns = [
url(r'^$', Index.as_view(), name='index'), # url(r'^$', Index.as_view(), name='index'),
url(r'^(?P<slug>[-\w]+)/$', EventView.as_view(), name='event'),
url(r'^(?P<slug>[-\w]+)/s/(?P<username>[-\w]+)/$', EventViewStaff.as_view(), name='event-staff'),
url(r'^(?P<slug>[-\w]+)/calendar/$', EventCalendar.as_view(), name='calendar'),
url(r'^activity/(?P<pk>[0-9]+)/$', ActivityView.as_view(),
name='activity'),
url(r'^activity/(?P<pk>[0-9]+)/enrol/$',
EnrolActivityView.as_view(), name="enrol_activity"),
] ]

View file

@ -1,5 +1,97 @@
from django.views.generic import TemplateView from django.views.generic import TemplateView, DetailView, View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404, render
from django.http import JsonResponse, HttpResponseRedirect
from django.urls import reverse
from .models import Event, Activity
from equipment.models import EquipmentAttribution
User = get_user_model()
class Index(TemplateView): class Index(TemplateView):
template_name = "event/index.html" template_name = "event/index.html"
class EventCalendar(LoginRequiredMixin, DetailView):
model = Event
template_name = 'event/calendar.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
event = self.object
context['activities'] = (Activity.objects.filter(event=event)
.order_by('beginning').prefetch_related(
'tags', 'places', 'staff', 'parent'))
return context
class EventView(LoginRequiredMixin, DetailView):
model = Event
template_name = 'event/event.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
event = self.object
context['activities'] = (Activity.objects.filter(event=event)
.order_by('beginning').prefetch_related(
'tags', 'places', 'staff', 'parent'))
return context
class EventViewStaff(LoginRequiredMixin, DetailView):
model = Event
template_name = 'event/event.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = User.objects.get(username=self.kwargs['username'])
event = self.object
context['staffuser'] = user
context['activities'] = (user.in_perm_activities
.filter(event=event)
.order_by('beginning')
.prefetch_related(
'tags', 'places', 'staff', 'parent')
)
return context
class ActivityView(LoginRequiredMixin, DetailView):
model = Activity
template_name = 'event/activity.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
activity = self.object
context['attributions'] = (EquipmentAttribution.objects
.filter(activity=activity)
.prefetch_related('equipment'))
return context
class EnrolActivityView(LoginRequiredMixin, View):
http_method_names = ['post']
def post(self, request, pk, *args, **kwargs):
activity = get_object_or_404(Activity, id=pk)
action = request.POST.get("goal", None)
success = True
if action == "enrol":
activity.staff.add(request.user)
elif action == "unenrol":
activity.staff.remove(request.user)
else:
success = False
if "ajax" in request.GET:
if request.GET["ajax"] == "json":
enrols = activity.staff
return JsonResponse({
"enrolled": enrols.filter(id=request.user.id).exists(),
"number": enrols.count(),
})
return render(request, "event/activity_summary.html",
{"activity": activity})
return HttpResponseRedirect(reverse("event:activity", kwargs={"pk":pk}))

View file

@ -3,7 +3,7 @@ import os
import sys import sys
if __name__ == "__main__": if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "evenementiel.settings.devlocal") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "poulpe.settings.devlocal")
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line

0
poulpe/__init__.py Normal file
View file

View file

@ -2,6 +2,6 @@ import os
from channels.asgi import get_channel_layer from channels.asgi import get_channel_layer
if "DJANGO_SETTINGS_MODULE" not in os.environ: if "DJANGO_SETTINGS_MODULE" not in os.environ:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "evenementiel.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "poulpe.settings")
channel_layer = get_channel_layer() channel_layer = get_channel_layer()

View file

@ -10,6 +10,8 @@ We also load the secrets in this file.
import os import os
from . import secret from . import secret
from django.urls import reverse_lazy
from django.contrib import messages
def import_secret(name): def import_secret(name):
@ -25,6 +27,7 @@ def import_secret(name):
SECRET_KEY = import_secret("SECRET_KEY") SECRET_KEY = import_secret("SECRET_KEY")
ADMINS = import_secret("ADMINS") ADMINS = import_secret("ADMINS")
SERVER_EMAIL = import_secret("SERVER_EMAIL")
DBNAME = import_secret("DBNAME") DBNAME = import_secret("DBNAME")
DBUSER = import_secret("DBUSER") DBUSER = import_secret("DBUSER")
@ -44,31 +47,46 @@ BASE_DIR = os.path.dirname(
INSTALLED_APPS = [ INSTALLED_APPS = [
'communication.apps.CommunicationConfig', # 'shared.apps.CustomAdminConfig',
'equipment.apps.EquipmentConfig',
'event.apps.EventConfig',
'users.apps.UsersConfig',
'shared.apps.SharedConfig',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'channels', 'django.contrib.sites',
'rest_framework',
# 'channels',
# 'rest_framework',
'bootstrapform', 'bootstrapform',
'widget_tweaks', 'widget_tweaks',
'api', 'taggit',
'django_tables2',
'django_filters',
'bootstrap3',
'allauth_ens',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth_cas',
'allauth_ens.providers.clipper',
# 'api',
'communication',
'equipment',
'event',
'shared',
'users',
] ]
MIDDLEWARE_CLASSES = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', # 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
@ -81,7 +99,7 @@ REST_FRAMEWORK = {
'TEST_REQUEST_DEFAULT_FORMAT': 'json', 'TEST_REQUEST_DEFAULT_FORMAT': 'json',
} }
ROOT_URLCONF = 'evenementiel.urls' ROOT_URLCONF = 'poulpe.urls'
STATIC_URL = "/static/" STATIC_URL = "/static/"
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
@ -119,19 +137,19 @@ DATABASES = {
} }
} }
CHANNEL_LAYERS = { # CHANNEL_LAYERS = {
"default": { # "default": {
"BACKEND": "asgi_redis.RedisChannelLayer", # "BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": { # "CONFIG": {
"hosts": [( # "hosts": [(
"redis://:{passwd}@{host}:{port}/{db}" # "redis://:{passwd}@{host}:{port}/{db}"
.format(passwd=REDIS_PASSWD, host=REDIS_HOST, # .format(passwd=REDIS_PASSWD, host=REDIS_HOST,
port=REDIS_PORT, db=REDIS_DB) # port=REDIS_PORT, db=REDIS_DB)
)], # )],
}, # },
"ROUTING": "evenementiel.routing.channel_routing", # "ROUTING": "poulpe.routing.channel_routing",
} # }
} # }
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
@ -148,12 +166,55 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/ # https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'UTC' TIME_ZONE = 'Europe/Paris'
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
SITE_ID = 1
CAS_SERVER_URL = "https://cas.eleves.ens.fr/" # SPI CAS
CAS_VERIFY_URL = "https://cas.eleves.ens.fr/"
CAS_VERSION = "2"
CAS_IGNORE_REFERER = True
CAS_LOGIN_MSG = None
CAS_REDIRECT_URL = reverse_lazy('shared:home')
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
CAS_FORCE_CHANGE_USERNAME_CASE = "lower"
LOGIN_URL = reverse_lazy('account_login')
LOGOUT_URL = reverse_lazy('account_logout')
LOGIN_REDIRECT_URL = reverse_lazy('shared:home')
ACCOUNT_HOME_URL = reverse_lazy('shared:home')
ACCOUNT_DETAILS_URL = reverse_lazy('shared:home')
SOCIALACCOUNT_PROVIDERS = {
# …
'clipper': {
# These settings control whether a message containing a link to
# disconnect from the CAS server is added when users log out.
'MESSAGE_SUGGEST_LOGOUT_ON_LOGOUT': True,
'MESSAGE_SUGGEST_LOGOUT_ON_LOGOUT_LEVEL': messages.INFO,
},
}
ACCOUNT_ADAPTER = 'shared.allauth_adapter.AccountAdapter'
#SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'
SOCIALACCOUNT_ADAPTER= 'shared.allauth_adapter.SocialAccountAdapter'

View file

@ -11,10 +11,12 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEBUG = True DEBUG = True
# Add some debugging tools # Add some debugging tools
INSTALLED_APPS += ["debug_toolbar", "debug_panel"] # NOQA INSTALLED_APPS += ["debug_toolbar", ] # NOQA
MIDDLEWARE_CLASSES = ( MIDDLEWARE = (
["debug_panel.middleware.DebugPanelMiddleware"] [
+ MIDDLEWARE_CLASSES # NOQA "debug_toolbar.middleware.DebugToolbarMiddleware",
]
+ MIDDLEWARE # NOQA
) )

View file

@ -18,6 +18,6 @@ DATABASES = {
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "asgiref.inmemory.ChannelLayer", "BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "evenementiel.routing.channel_routing", "ROUTING": "poulpe.routing.channel_routing",
}, },
} }

View file

@ -5,11 +5,23 @@ from django.conf import settings
from django.conf.urls import url, include from django.conf.urls import url, include
from django.contrib import admin from django.contrib import admin
#from django_cas_ng import views as django_cas_views
from allauth_ens.views import capture_login, capture_logout
urlpatterns = [ urlpatterns = [
url(r'^admin/login/$', capture_login),
url(r'^admin/logout/$', capture_logout),
url(r'^compte/', include('allauth.urls')),
# Admin
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
# Apps
url(r'^equipment/', include('equipment.urls')),
url(r'^event/', include('event.urls')), url(r'^event/', include('event.urls')),
url(r'^user/', include('users.urls')), #url(r'^user/', include('users.urls')),
# REST
url(r'^api/', include('api.urls')), url(r'^api/', include('api.urls')),
# Reste
url(r'^', include('shared.urls')), url(r'^', include('shared.urls')),
] ]

View file

@ -1,5 +1,5 @@
""" """
WSGI config for evenementiel project. WSGI config for GestionEvenementiel project.
It exposes the WSGI callable as a module-level variable named ``application``. It exposes the WSGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "evenementiel.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "poulpe.settings")
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -10,7 +10,7 @@ DBPASSWD="4KZt3nGPLVeWSvtBZPsd9jdssdJMds78"
REDIS_PASSWD="dummy" REDIS_PASSWD="dummy"
# It is used in quite a few places # It is used in quite a few places
SETTINGS="evenementiel.settings.dev" SETTINGS="poulpe.settings.dev"
# Fills a "templated file" with the information specified in the variables above # Fills a "templated file" with the information specified in the variables above
# e.g. every occurrence of {{DBUSER}} in the file will be replaced by the value # e.g. every occurrence of {{DBUSER}} in the file will be replaced by the value
@ -90,9 +90,9 @@ redis-cli -a $REDIS_PASSWD CONFIG REWRITE
cd /vagrant cd /vagrant
# Setup the secrets # Setup the secrets
sudo -H -u vagrant cp evenementiel/settings/secret_example.py \ sudo -H -u vagrant cp poulpe/settings/secret_example.py \
evenementiel/settings/secret.py poulpe/settings/secret.py
fill_template evenementiel/settings/secret.py fill_template poulpe/settings/secret.py
# Run the usual django admin commands # Run the usual django admin commands
function venv_python { function venv_python {

View file

@ -10,7 +10,7 @@ TimeoutSec=300
WorkingDirectory=/vagrant WorkingDirectory=/vagrant
Environment="DJANGO_SETTINGS_MODULE={{SETTINGS}}" Environment="DJANGO_SETTINGS_MODULE={{SETTINGS}}"
ExecStart=/home/vagrant/venv/bin/daphne -u /srv/GE/GE.sock \ ExecStart=/home/vagrant/venv/bin/daphne -u /srv/GE/GE.sock \
evenementiel.asgi:channel_layer poulpe.asgi:channel_layer
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -1,4 +1,3 @@
-r requirements.txt -r requirements.txt
django-debug-toolbar django-debug-toolbar
django-debug-panel
ipython ipython

6
requirements-prod.txt Normal file
View file

@ -0,0 +1,6 @@
-r requirements.txt
# Production specific
daphne==1.3.0
psycopg2
gunicorn

View file

@ -1,14 +1,14 @@
Django==1.11.* asgi-redis==1.3.0
psycopg2 asgiref==1.1.1
asgi-redis django-bootstrap3==10.0.1
Pillow channels==1.1.5
channels Django==2.1
django-bootstrap-form==3.2.1 django-tables2==2.0.0a2
django-widget-tweaks django-filter==2.0.0
djangorestframework==3.6.3 git+https://git.eleves.ens.fr/klub-dev-ens/django-allauth-ens.git@1.1.3
django-bootstrap-form==3.4
drf-nested-routers==0.90.0 drf-nested-routers==0.90.0
django-notifications django-notifications==0.1.dev0
django-contrib-comments django-contrib-comments==1.8.0
django-taggit==0.22.2
# Production specific Pillow==5.0.0
daphne

View file

@ -0,0 +1 @@
default_app_config = 'shared.apps.SharedConfig'

View file

@ -1,3 +1,34 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.admin import AdminSite
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
# Register your models here. from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin, GroupAdmin
User = get_user_model()
class CustomAdminSite(AdminSite):
site_header = "Administration du Poulpe"
site_title = "Poulpe"
index_title = "Administration"
def index(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
# Move last app to the top of `app_list`.
# TODO fournir un bon ordre
app_list = self.get_app_list(request)
app_list.insert(0, app_list.pop(-1))
extra_context['app_list'] = app_list
return super().index(request, extra_context)
# admin.site = CustomAdminSite(name='admin')
admin.site.register(User, UserAdmin)
# admin.site.register(Group, GroupAdmin)
# admin.site.register(Site, SiteAdmin)

20
shared/allauth_adapter.py Normal file
View file

@ -0,0 +1,20 @@
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class AccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return False
class SocialAccountAdapter(DefaultSocialAccountAdapter):
def is_open_for_signup(self, request, sociallogin):
# sociallogin.account is a SocialAccount instance.
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/models.py
if sociallogin.account.provider == 'clipper':
return True
# It returns AccountAdapter.is_open_for_signup().
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/adapter.py
return super().is_open_for_signup(request, sociallogin)

View file

@ -1,5 +1,10 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.contrib.admin.apps import AdminConfig
class SharedConfig(AppConfig): class SharedConfig(AppConfig):
name = 'shared' name = 'shared'
class CustomAdminConfig(AdminConfig):
default_site = 'shared.admin.CustomAdminSite'

View file

@ -11,6 +11,7 @@ class EventSpecificMixin(models.Model):
verbose_name=_("évènement"), verbose_name=_("évènement"),
help_text=_("Si spécifié, l'instance du modèle" help_text=_("Si spécifié, l'instance du modèle"
"est spécifique à l'évènement en question"), "est spécifique à l'évènement en question"),
on_delete=models.CASCADE,
blank=True, blank=True,
null=True null=True
) )

25
shared/static/config.rb Normal file
View file

@ -0,0 +1,25 @@
require 'compass/import-once/activate'
# Require any additional compass plugins here.
# Set this to the root of your project when deployed:
http_path = "/"
css_dir = "css"
sass_dir = "sass"
images_dir = "images"
javascripts_dir = "javascripts"
# You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed
# To enable relative paths to assets via compass helper functions. Uncomment:
# relative_assets = true
# To disable debugging comments that display the original location of your selectors. Uncomment:
# line_comments = false
# If you prefer the indented syntax, you might want to regenerate this
# project again passing --syntax sass, or you can uncomment this:
# preferred_syntax = :sass
# and then run:
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass

View file

@ -0,0 +1,114 @@
//@import "compass/css3";
//Variables here:
//(alongside with commented suggestions)
$foreground-color:#b85b3f;//black;
$background-color:#e8e3c7;//white
$shadow-color:#ba9186;//$foreground-color;
$distance:8px;
$cut-distance:3px;//$distance/4;
$strips-size:6px; //10px
$strips-ratio:50%;//70%
$strips-angle:45deg;//90deg;
//cray stuff yo. be sure to try (if you please)
$animate:false;//true
$fixed:false;//true
body{
font-family: 'Open Sans Condensed', sans-serif;
font-size:85pt;
background-color:$background-color;
text-align:center;
line-height:1.2em;
padding-top:70px;
}
.dashed-shadow{
position:relative;
top:$distance;
left:$distance;
display:inline-block;
color:$shadow-color;
}
.dashed-shadow:before{
content:" ";
display:block;
position:absolute;
$bleeding-horizontal:10px;
$bleeding-vertical:0px;
top:-$bleeding-vertical - $distance;
left:-$bleeding-vertical - $distance;
bottom:-$bleeding-horizontal + $distance;
right:-$bleeding-horizontal + $distance;
z-index:1;
$color:$background-color;
$size:$strips-ratio/2;
$halfSize:$size/2;
$p1:$halfSize;
$p2:50%-$halfSize;
$p3:50%+$halfSize;
$p4:100%-$halfSize;
$transparent:transparentize($color,1);
@include background-image(linear-gradient($strips-angle,$color $p1, $transparent $p1, $transparent $p2,$color $p2, $color $p3, $transparent $p3, $transparent $p4, $color $p4));
background-size:$strips-size $strips-size;
@if($animate){
animation:dash-animation 30s infinite linear;
}
@if($fixed){
background-attachment:fixed;
}
}
.dashed-shadow:hover:before{
animation:dash-animation 30s infinite linear;
}
.dashed-shadow:after{
z-index:2;
content:attr(data-text);
position:absolute;
left:-$distance;
top:-$distance;
color:$foreground-color;
text-shadow:$cut-distance $cut-distance $background-color;
}
//fancy stuff - just useless fluff, don't mind from here onwards
.hello{
font-family:'Cookie',cursive;
font-size:140pt;
}
.sorta-block{
font-size:50pt;
line-height:1.1em;
@include transform(skew(0,-5deg));
z-index:3;
position:relative;
margin-top:20px;
margin-bottom:10px;
}
.sorta{
border-top:4px solid $foreground-color;
border-bottom:4px solid $foreground-color;
text-transform:uppercase;
z-index:3;
//position:relative;
//display:block;
//width:300px;
font-style:italic;
}
.hipsterish{
font-family: 'Sancreek', cursive;
font-size:70pt;
}
.dashed-shadow-text{
font-size:140pt;
line-height:0.7em;
//left:-10px;
}
.shadow{
font-size:120pt;
line-height:0.8em;
}

View file

@ -0,0 +1,18 @@
// main: global.scss
#filter_form {
.form-group {
.col-md-3, .col-md-9 {
float: none;
}
ul.form-control {
padding-left: 15px;
list-style: none;
height: auto;
a.selected {
text-decoration: underline;
color: darken($main_soft_color, 40%);
}
}
}
}

View file

@ -0,0 +1,122 @@
// main: global.scss
@mixin active {
&:active,
&.active {
@content;
}
}
@mixin hover-focus {
&:focus,
&.focus,
&:hover {
@content;
}
}
@mixin disabled {
&.disabled,
&[disabled],
fieldset[disabled] & {
@content;
}
}
@mixin dropdown-open {
.open > &.dropdown-toggle { @content }
}
@mixin btn-special {
/**
* This mixin applies content if the button is in at least one of the
* following states:
*
* - hovered,
* - focused,
* - actived,
* - is responsible of an opened dropdown.
*
* Where possible, state is checked from class attribute and
* :pseudo-classes.
*
* ## Bootstrap compatibility
*
* If content defines 'color', 'background-color' and 'border', it is safe
* to use this mixin with Bootstrap buttons as it will overrides all
* Bootstrap color defaults of the previous cases.
* To be precise, this covers all special important-like cases of the
* Bootstrap mixin 'button-variant' (except the 'disabled' case).
*
*/
@include hover-focus { @content }
@include active {
@content;
@include hover-focus { @content }
}
@include dropdown-open {
@content;
@include hover-focus { @content }
}
}
@mixin button-variant-2modes($color, $background-base, $background-special, $border) {
/**
* This mixins allows defining color-related properties of buttons.
*
* It sets the following properties:
* color: $color, except for disabled-like buttons.
* border-color: $border.
* background-color: Depending on button state:
* - Default, disabled:
* $background-base
* - Hovered, focused, actived, responsible of an opened dropdown:
* (one is sufficent)
* $background-special
*
* ## Bootstrap compatibility
*
* This mixin can be used to replace colors behaviors of Bootstrap buttons.
* Indeed, this mixin aims to replace each definition done by the
* 'button-variant' Bootstrap mixin.
*
*/
color: $color;
background-color: $background-base;
border-color: $border;
@include btn-special {
color: $color;
background-color: $background-special;
border-color: $border;
}
@include disabled {
@include hover-focus {
background-color: $background-base;
border-color: $border;
}
}
.badge {
color: $background-base;
background-color: $color;
}
}
.btn-primary {
@include button-variant-2modes($btn-font-color, $btn-bg-base, $btn-bg-special, $btn-border);
}
form#filter_form {
.form-group {
padding-right:20px;
}
ul.form-control {
background-color: transparent;
border: none;
box-shadow: none;
}
}

View file

@ -0,0 +1,91 @@
// main: global.scss
.strong-banner {
padding-top : 20px;
padding-bottom : 10px ;
background-color : $header-background;
color: $header-color;
}
.navbar-inverse {
background-color : $header-background;
background-color : transparent ;
border-style : none ;
.navbar-nav {
& > .open > a,
& > .open > a:focus,
& > .open > a:hover {
color: #fff;
background-color: $header-second-backgroud;
}
}
}
.navbar-collapse {
border-top: 0px solid transparent ;
padding : 0px ;
/* only < 768px*/
background-color : $header-second-backgroud;
padding-left: 25px;
margin-left: -15px;
margin-right: -15px;
@media (min-width: 768px) {
background-color : transparent;
padding-left: 0px;
margin-left: 0px;
margin-right: 0px;
}
}
.navbar-nav {
width: 100%;
@media (min-width: 768px) {
float : right ;
width: auto;
}
}
.navbar-inverse {
/* BRAND */
.navbar-brand {
&,
&:hover,
&:focus {
color: white;
font-family: $font_brand;
font-size: xx-large;
border-bottom: 5px solid $underline-brand;
}
}
/* ICONE */
.navbar-toggle {
&,
&:hover,
&:focus {
background-color: $second_bold_color;
border-color: $second_bold_color;
}
.icon-bar {
background-color: $second_white_color;
}
}
/* LINKS */
.navbar-nav {
& > li > a {
&,
&:hover,
&:focus {
font-family: $font_nav;
font-size: large;
color: $second_bold_color;
background: transparent;
@media (min-width: 768px) {
color: $second_white_color;
}
}
}
}
}

View file

@ -0,0 +1,24 @@
// main: global.scss
.message-info {
color : #31708f;
background-color: #d9edf7;
border-color : #bce8f1;
}
.message-success {
color : #3c763d;
background-color: #dff0d8;
border-color : #d6e9c6;
}
.message-warning {
color : #8a6d3b;
background-color: #fcf8e3;
border-color : #faebcc;
}
.message-error {
color : #a94442;
background-color: #f2dede;
border-color : #ebccd1;
}
.alert {
margin-bottom: 0;
}

View file

@ -0,0 +1,38 @@
// main: global.scss
.tree {
font-size:large;
ul, li {
position: relative;
}
ul {
list-style: none;
padding-left: 32px;
}
li::before,
li::after {
content: "";
position: absolute;
left: -12px;
}
li::before {
border-top: 3px solid $second_soft_color;
top: 9px;
width: 8px;
height: 0;
}
li::after {
border-left: 3px solid $second_soft_color;
height: 100%;
width: 0px;
top: 2px;
}
ul > li:last-child::after {
height: 8px;
}
.category_node {
}
}

View file

@ -0,0 +1,99 @@
// main: global.scss
$main_c7: #375362;
$main_c6: #4F778C;
$main_c5: #5D8CA6;
$main_c3: #BDD2DE;
$main_c1: #F0FAFF;
$neutral_c2 : #F2EDDC;
$neutral_c1: #FFFBEF;
$activity_c8: #4FADB8;
$activity_c7: #5ED1DC;
$activity_c3: #CAE4E7;
$event_c8: #3488A6;
$event_c7: #3999BA;
$event_c3: #AAD5E2;
$todo_c8: #F19F5D;
$todo_c7: #FF9C4D;
$todo_c3: #FFDEBC;
$equipment_c8: #E75571;
$equipment_c7: #FF5C79;
$equipment_c3: #FECAD6;
$staff_c8: #3BAD89;
$staff_c7: #42C2A2;
$staff_c3: #A9E1D7;
/* LEGACY COLORS*/
$main_bold_color: #FF6969;
$main_soft_color: #FF9191;
$main_white_color: #FFEBEB;
$second_bold_color: #FFB363;
$second_soft_color: #FFC282;
$second_white_color: #FFF5EB;
$third_bold_color: #48B0C7;
$third_soft_color: #8FD4E3;
$third_white_color: #DCEAED;
/* COLORS */
/* Header */
$header-background: $main_c7;
$header-second-backgroud: $main_c5;
$header-color: white;
$underline-brand: $equipment_c7;
/* Général */
$html-background: $neutral_c1;
$content-background: $neutral_c1;
$content-border: $main_c7;
$aside-background: $main_c7;
$aside-inverted-background: $main_c5;
$title_border: $main_c7;
/* Le reste */
$btn-font-color: white;
$btn-bg-base: $main_bold_color;
$btn-bg-special: $main_soft_color;
$btn-border: $main_bold_color;
$yes_color: #55C487;
$no_color: #E36268;
$dunno_color: #5599C4;
/* Titres */
$h1_background: $main_c6;
$h2_background: $main_c3;
$h3_background: $main_c1;
$h1_background_activity: $activity_c8;
$h2_background_activity: $activity_c7;
$h3_background_activity: $main_c1;
$h1_background_event: $event_c8;
$h2_background_event: $event_c7;
$h3_background_event: $main_c1;
$h1_background_todo: $todo_c8;
$h2_background_todo: $todo_c7;
$h3_background_todo: $main_c1;
$h1_background_equipment: $equipment_c8;
$h2_background_equipment: $equipment_c7;
$h3_background_equipment: $main_c1;
$h1_background_staff: $staff_c8;
$h2_background_staff: $staff_c7;
$h3_background_staff: $main_c1;
/* FONTS */
$font_brand:'Lily Script One', cursive;
$font_nav:'Work Sans', cursive;
$font_bold:'Capriola', sans-serif;
$font_normal:'Saira Semi Condensed', sans-serif;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
global.min.css

File diff suppressed because one or more lines are too long

1
shared/static/css/global.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,416 @@
/*NE PAS MODIFIER LE FICHIER .CSS, MAIS PLUTÔT LE
FICHIER .SCSS */
@import '_variables';
@import '_messages';
@import '_header';
@import '_forms';
@import '_tree';
@import '_filters';
//@import '_dashed-shadows';
/* MISE EN FORME GÉNÉRALE */
html {
height : 100% ;
background-color: $html-background;
}
body {
font-family: $font_normal;
font-size: medium;
}
#principal {
background-color: $html-background;
}
/*MAIN*/
main {
background-color:$content-background;
border-width: 2px;
border-color: $content-border;
border-style: none;
border-collapse: collapse;
display: table-cell;
margin-top:0px;
padding: 0px;
.fuid, h1, h2, h3 {
margin-left: -15px;
margin-right: -15px;
margin-top: 0px;
margin-bottom: 0px;
}
h1, h2, h3 {
margin-top: -2px;
margin-bottom: -2px;
margin-left: -17px;
margin-right: -17px;
color: white;
padding-left: 15px;
padding-right: 15px;
}
h1 {
font-family: $font_bold;
font-weight: 600;
padding-bottom: 10px;
padding-top: 15px;
background-color: $h1_background;
&.activity {
background-color: $h1_background_activity;
}
&.event {
background-color: $h1_background_event;
}
&.todo {
background-color: $h1_background_todo;
}
&.equipment {
background-color: $h1_background_equipment;
}
&.staff {
background-color: $h1_background_staff;
}
}
h2, h3 {
border-bottom : 2px solid $main_c5;
padding-bottom : 5px ;
padding-top: 10px;
font-family: $font_bold;
font-weight: 600;
font-size: x-large;
background-color: $h2_background;
&.activity {
background-color: $h2_background_activity;
border-color: $activity_c8;
}
&.event {
background-color: $h2_background_event;
border-color: $event_c8;
}
&.todo {
background-color: $h2_background_todo;
border-color: $todo_c8;
}
&.equipment {
background-color: $h2_background_equipment;
border-color: $equipment_c8;
}
&.staff {
background-color: $h2_background_staff;
border-color: $staff_c8;
}
}
h3 {
background-color: $h3_background;
color: $main_c6;
&.activity {
background-color: $h3_background_activity;
color: $activity_c7;
}
&.event {
background-color: $h3_background_event;
color: $event_c7;
}
&.todo {
background-color: $h3_background_todo;
color: $todo_c7;
}
&.equipment {
background-color: $h3_background_equipment;
color: $equipment_c7;
}
&.staff {
background-color: $h3_background_staff;
color: $staff_c7;
}
}
a {
color: $main_bold_color;
&:hover,
&:active,
&:focus {
color : $main_bold_color;
}
}
.text {
padding: 15px;
}
}
/*ASIDE*/
aside {
background-color:$aside-background;
color: white;
margin-top: 0px;
padding: 0px!important;
a {
color: $main_soft_color;
&:hover,
&:active,
&:focus {
color : $main_soft_color;
}
}
code {
color: $main_c7;
background-color: $main_c3;
}
.heading {
padding: 8px 15px;
font-size: 32px;
line-height: 1.3;
text-align:center;
&.inverted {
background-color:$second_white_color;
color:black;
}
&.small {
font-size: 25px;
.sub {
font-size: 0.7em;
font-weight: normal;
}
}
.sub {
font-size: 0.7em;
font-weight: normal;
}
&.separator {
border-bottom-color: $main_soft_color;
border-bottom-style: solid;
}
}
.text {
padding: 15px;
&.inverted {
background-color:$second_white_color;
color:black;
}
}
}
@media (min-width: 768px) {
main {
margin-top:20px;
border-style: dashed;
}
aside {
margin-top:20px;
}
}
hr {
border-top : 1px solid $second_bold_color ;
}
span.vsep {
padding-left: 5px;
padding-right: 5px;
}
div.tag-list {
margin-top: 20px;
}
code {
font-size: small;
}
.module-list {
margin-top: 10px;
margin-bottom: 10px;
display: flex;
align-items: stretch;
flex-wrap: wrap;
}
a.module {
padding: 20px 40px;
margin: 5px;
border-bottom-style: solid;
font-size: large;
display: block;
border-bottom-color: $main_c7;
background-color: $main_c3;
color: $main_c7;
&.activity {
border-bottom-color: $activity_c8;
background-color: $activity_c3;
color: $activity_c8;
}
&.event {
border-bottom-color: $event_c8;
background-color: $event_c3;
color: $event_c8;
}
&.todo {
border-bottom-color: $todo_c8;
background-color: $todo_c3;
color: $todo_c8;
}
&.equipment {
border-bottom-color: $equipment_c8;
background-color: $equipment_c3;
color: $equipment_c8;
}
&.staff {
border-bottom-color: $staff_c8;
background-color: $staff_c3;
color: $staff_c8;
}
&:hover,
&:active,
&:focus {
text-decoration: none;
color: $main_c1;
background-color: $main_c7;
&.activity {
background-color: $activity_c7;
}
&.event {
background-color: $event_c7;
}
&.equipment {
background-color: $equipment_c7;
}
&.todo {
background-color: $todo_c7;
}
&.staff {
background-color: $staff_c7;
}
}
}
.collapsible {
background-color: #777;
color: white;
cursor: pointer;
padding: 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
&:after {
content: '\002B';
color: white;
font-weight: bold;
float: right;
margin-left: 5px;
}
&:hover {
background-color: #555;
}
h3 {
margin-top: 0px;
margin-bottom: 0px;
display: inline-block;
}
}
.active {
background-color: #555;
&:after {
content: "\2212";
}
}
.content {
padding: 8px 18px;
display : none;
background-color: #f1f1f1;
&.fluid {
padding: 0px 0px;
background-color: transparent;
}
h4 {
font-size: x-large;
font-weight: bold;
display: inline-block;
margin: 0px 0px;
}
}
.planning {
.activity {
padding: 8px 18px;
padding-top: 8px;
padding-bottom: 12px;
border-left: none;
border-right: 6px solid $main_soft_color;
&.inverted {
border-left: 6px solid $main_soft_color;
border-right: none;
}
.activity-title {
font-size: large;
}
}
}
.glyphicon {
&.yes {
color:$yes_color!important;
}
&.no {
color:$no_color!important;
}
&.dunno {
color:$dunno_color!important;
}
}
.sending-request {
position: relative;
&:after {
content: "Chargement...";
position: absolute;
width: 100%;
height: 100%;
background: #fff;
opacity: 0.8;
color: #777;
text-align: center;
box-sizing: border-box;
z-index: 5;
top: 0;
left: 0;
padding: 8%;
}
}

View file

@ -0,0 +1,71 @@
/*
Error: Undefined variable: "$main_bold_color".
on line 6 of header.scss
1: /* BANNER *\/
2:
3: .strong-banner {
4: padding-top : 20px;
5: padding-bottom : 10px ;
6: background-color : $main_bold_color;
7: color: $second_bold_color;
8: }
9:
10:
11: .navbar-inverse {
Backtrace:
header.scss:6
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/script/tree/variable.rb:49:in `_perform'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/script/tree/node.rb:50:in `perform'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:402:in `visit_prop'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/base.rb:36:in `visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:162:in `block in visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/stack.rb:79:in `block in with_base'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/stack.rb:135:in `with_frame'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/stack.rb:79:in `with_base'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:162:in `visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:444:in `block (2 levels) in visit_rule'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:444:in `map'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:444:in `block in visit_rule'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:183:in `with_environment'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:442:in `visit_rule'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/base.rb:36:in `visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:162:in `block in visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/stack.rb:79:in `block in with_base'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/stack.rb:135:in `with_frame'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/stack.rb:79:in `with_base'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:162:in `visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/base.rb:52:in `block in visit_children'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/base.rb:52:in `map'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/base.rb:52:in `visit_children'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:171:in `block in visit_children'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:183:in `with_environment'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:170:in `visit_children'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/base.rb:36:in `block in visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:190:in `visit_root'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/base.rb:36:in `visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:161:in `visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/visitors/perform.rb:10:in `visit'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/root_node.rb:36:in `css_tree'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/tree/root_node.rb:29:in `render_with_sourcemap'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/engine.rb:389:in `_render_with_sourcemap'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/engine.rb:307:in `render_with_sourcemap'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/plugin/compiler.rb:462:in `update_stylesheet'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/plugin/compiler.rb:215:in `block in update_stylesheets'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/plugin/compiler.rb:209:in `each'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/plugin/compiler.rb:209:in `update_stylesheets'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/plugin/compiler.rb:294:in `watch'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/plugin.rb:109:in `method_missing'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/exec/sass_scss.rb:360:in `watch_or_update'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/exec/sass_scss.rb:51:in `process_result'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/exec/base.rb:52:in `parse'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/lib/sass/exec/base.rb:19:in `parse!'
/usr/lib/ruby/gems/2.5.0/gems/sass-3.5.5/bin/sass:13:in `<top (required)>'
/usr/bin/sass:23:in `load'
/usr/bin/sass:23:in `<main>'
*/
body:before {
white-space: pre;
font-family: monospace;
content: "Error: Undefined variable: \"$main_bold_color\".\A on line 6 of header.scss\A \A 1: /* BANNER */\A 2: \A 3: .strong-banner {\A 4: padding-top : 20px;\A 5: padding-bottom : 10px ;\A 6: background-color : $main_bold_color;\A 7: color: $second_bold_color;\A 8: }\A 9: \A 10: \A 11: .navbar-inverse {"; }

View file

@ -1,126 +0,0 @@
@media (max-width: 768px) {
.header {
position: absolute;
}
/*sidebar*/
#sidebar {
height: auto;
overflow: hidden;
position: absolute;
width: 100%;
z-index: 1001;
}
/* body container */
#main-content {
margin: 0px!important;
position: none !important;
}
#sidebar > ul > li > a > span {
line-height: 35px;
}
#sidebar > ul > li {
margin: 0 10px 5px 10px;
}
#sidebar > ul > li > a {
height:35px;
line-height:35px;
padding: 0 10px;
text-align: left;
}
#sidebar > ul > li > a i{
/*display: none !important;*/
}
#sidebar ul > li > a .arrow, #sidebar > ul > li > a .arrow.open {
margin-right: 10px;
margin-top: 15px;
}
#sidebar ul > li.active > a .arrow, #sidebar ul > li > a:hover .arrow, #sidebar ul > li > a:focus .arrow,
#sidebar > ul > li.active > a .arrow.open, #sidebar > ul > li > a:hover .arrow.open, #sidebar > ul > li > a:focus .arrow.open{
margin-top: 15px;
}
#sidebar > ul > li > a, #sidebar > ul > li > ul.sub > li {
width: 100%;
}
#sidebar > ul > li > ul.sub > li > a {
background: transparent !important ;
}
#sidebar > ul > li > ul.sub > li > a:hover {
}
/* sidebar */
#sidebar {
margin: 0px !important;
}
/* sidebar collabler */
#sidebar .btn-navbar.collapsed .arrow {
display: none;
}
#sidebar .btn-navbar .arrow {
position: absolute;
right: 35px;
width: 0;
height: 0;
top:48px;
border-bottom: 15px solid #282e36;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
}
/*---------*/
.btn {
margin-bottom: 5px;
}
ul.sidebar-menu li ul.sub li a {
padding: 0;
}
/*---*/
.img-responsive {
width: 100%;
}
}
@media (max-width: 480px) {
#top_menu .nav > li, ul.top-menu > li {
float: right;
}
.hidden-phone {
display: none !important;
}
}
@media (max-width:320px) {
#top_menu .nav > li, ul.top-menu > li {
float: right;
}
.hidden-phone {
display: none !important;
}
}

View file

@ -1,918 +0,0 @@
/* Import fonts */
@import url(http://fonts.googleapis.com/css?family=Ruda:400,700,900);
/* BASIC THEME CONFIGURATION */
body {
color: #797979;
background: #f2f2f2;
font-family: 'Ruda', sans-serif;
padding: 0px !important;
margin: 0px !important;
font-size:13px;
}
ul li {
list-style: none;
}
a, a:hover, a:focus {
text-decoration: none;
outline: none;
}
::selection {
background: #FF9C77;
color: #fff;
}
::-moz-selection {
background: #FF9C77;
color: #fff;
}
#container {
width: 100%;
height: 100%;
}
/* Bootstrap Modifications */
.modal-header {
background: #FF9C77;
}
.modal-title {
color: white;
}
.btn-round {
border-radius: 20px;
-webkit-border-radius: 20px;
}
.accordion-heading .accordion-toggle {
display: block;
cursor: pointer;
border-top: 1px solid #F5F5F5;
padding: 5px 0px;
line-height: 28.75px;
text-transform: uppercase;
color: #1a1a1a;
background-color: #ffffff;
outline: none !important;
text-decoration: none;
}
/* centered columns styles */
.row-centered {
text-align:center;
}
.col-centered {
display:inline-block;
float:none;
/* reset the text-align */
text-align:left;
/* inline-block space fix */
}
/*Helpers*/
.centered {
text-align: center;
}
.goleft {
text-align: left;
}
.goright {
text-align: right;
}
.mt {
margin-top: 25px;
}
.mb {
margin-bottom: 25px;
}
.ml {
margin-left: 5px;
}
.no-padding {
padding: 0 !important;
}
.no-margin {
margin: 0 !important;
}
/*sidebar navigation*/
#sidebar {
width: 210px;
height: 100%;
position: fixed;
background: #424a5d;
}
#sidebar h5 {
color: #f2f2f2;
font-weight: 700;
}
#sidebar ul li {
position: relative;
}
#sidebar .sub-menu > .sub li {
padding-left: 32px;
}
#sidebar .sub-menu > .sub li:last-child {
padding-bottom: 10px;
}
/*LEFT NAVIGATION ICON*/
.dcjq-icon {
height:17px;
width:17px;
display:inline-block;
background: url("../img/nav-expand.png") no-repeat top;
border-radius:3px;
-moz-border-radius:3px;
-webkit-border-radius:3px;
position:absolute;
right:10px;
top:15px;
}
.active .dcjq-icon {
background: url("../img/nav-expand.png") no-repeat bottom;
border-radius:3px;
-moz-border-radius:3px;
-webkit-border-radius:3px;
}
/*---*/
.nav-collapse.collapse {
display: inline;
}
ul.sidebar-menu , ul.sidebar-menu li ul.sub{
margin: -2px 0 0;
padding: 0;
}
ul.sidebar-menu {
margin-top: 75px;
}
.sidebar-menu hr {
margin: 5px 10px;
border-color: #818899;
}
.sidebar-menu .banner-text {
text-transform: uppercase;
display: inline-block;
border-color: white;
border-width: 1px 0px;
border-style: solid;
margin: 15px;
padding: 5px;
}
.sidebar-menu .username {
color: #818899!important;
}
#sidebar > ul > li > ul.sub {
display: none;
}
#sidebar > ul > li.active > ul.sub, #sidebar > ul > li > ul.sub > li > a {
display: block;
}
ul.sidebar-menu li ul.sub li{
background: #424a5d;
margin-bottom: 0;
margin-left: 0;
margin-right: 0;
}
ul.sidebar-menu li ul.sub li:last-child{
border-radius: 0 0 4px 4px;
-webkit-border-radius: 0 0 4px 4px;
}
ul.sidebar-menu li ul.sub li a {
font-size: 12px;
padding: 6px 0;
line-height: 35px;
height: 35px;
-webkit-transition: all 0.3s ease;
-moz-transition: all 0.3s ease;
-o-transition: all 0.3s ease;
-ms-transition: all 0.3s ease;
transition: all 0.3s ease;
color: #aeb2b7;
}
ul.sidebar-menu li ul.sub li a:hover {
color: white;
background: transparent;
}
ul.sidebar-menu li ul.sub li.active a {
color: #FF9C77;
-webkit-transition: all 0.3s ease;
-moz-transition: all 0.3s ease;
-o-transition: all 0.3s ease;
-ms-transition: all 0.3s ease;
transition: all 0.3s ease;
display: block;
}
ul.sidebar-menu li{
/*line-height: 20px !important;*/
margin-bottom: 5px;
margin-left:10px;
margin-right:10px;
}
ul.sidebar-menu li.sub-menu{
line-height: 15px;
}
ul.sidebar-menu li a span{
display: inline-block;
}
ul.sidebar-menu li a{
color: #aeb2b7;
text-decoration: none;
display: block;
padding: 15px 0 15px 10px;
font-size: 12px;
outline: none;
-webkit-transition: all 0.3s ease;
-moz-transition: all 0.3s ease;
-o-transition: all 0.3s ease;
-ms-transition: all 0.3s ease;
transition: all 0.3s ease;
}
ul.sidebar-menu li a.active, ul.sidebar-menu li a:hover, ul.sidebar-menu li a:focus {
background: #FF9C77;
color: #fff;
display: block;
-webkit-transition: all 0.3s ease;
-moz-transition: all 0.3s ease;
-o-transition: all 0.3s ease;
-ms-transition: all 0.3s ease;
transition: all 0.3s ease;
}
ul.sidebar-menu li a i {
font-size: 15px;
padding-right: 6px;
}
ul.sidebar-menu li a:hover i, ul.sidebar-menu li a:focus i {
color: #fff;
}
ul.sidebar-menu li a.active i {
color: #fff;
}
/* MAIN CONTENT CONFIGURATION */
#main-content {
margin-left: 210px;
}
.header, .footer {
min-height: 60px;
padding: 0 15px;
}
.header {
position: fixed;
left: 0;
right: 0;
z-index: 1002;
}
.black-bg {
background: #ffd777;
border-bottom: 1px solid #c9aa5f;
}
.wrapper {
display: inline-block;
margin-top: 60px;
padding-left: 15px;
padding-right: 15px;
padding-bottom: 15px;
padding-top: 0px;
width: 100%;
}
a.logo {
font-size: 20px;
color: #ffffff;
float: left;
margin-top: 15px;
text-transform: uppercase;
}
a.logo b {
font-weight: 900;
}
a.logo:hover, a.logo:focus {
text-decoration: none;
outline: none;
}
a.logo span {
color: #FF9C77;
}
/*
* top links
*/
.navbar-top-links li {
display: inline-block;
}
.navbar-top-links li:last-child {
margin-right: 15px;
}
.navbar-top-links li > a {
padding: 20px 10px;
min-height: 60px;
color: #797979;
}
.navbar-top-links .open > a,
.navbar-top-links .open > a:hover,
.navbar-top-links .open > a:focus,
.navbar-top-links .open > a:active {
background-color:#C9AA5F;
}
.navbar-top-links li > a:hover,
.navbar-top-links li > a:focus,
.navbar-top-links li > a:active{
background-color:inherit;
}
.navbar-top-links .dropdown-menu li {
display: block;
}
.navbar-top-links .dropdown-menu li:last-child {
margin-right: 0;
}
.navbar-top-links .dropdown-menu li a {
padding: 3px 20px;
min-height: 0;
}
.navbar-top-links .dropdown-menu li a div {
white-space: normal;
}
.navbar-top-links .dropdown-messages,
.navbar-top-links .dropdown-tasks,
.navbar-top-links .dropdown-alerts {
width: 310px;
min-width: 0;
}
.navbar-top-links .dropdown-messages {
margin-left: 5px;
}
.navbar-top-links .dropdown-tasks {
margin-left: -59px;
}
.navbar-top-links .dropdown-alerts {
margin-left: -123px;
}
.navbar-top-links .dropdown-user {
right: 0;
left: auto;
}
/*--sidebar toggle---*/
.sidebar-toggle-box {
float: left;
padding-right: 15px;
margin-top: 20px;
}
.sidebar-toggle-box .fa-bars {
cursor: pointer;
display: inline-block;
font-size: 20px;
}
.sidebar-closed > #sidebar > ul {
display: none;
}
.sidebar-closed #main-content {
margin-left: 0px;
}
.sidebar-closed #sidebar {
margin-left: -180px;
}
/*
* Dash Side
* barre de droite
*/
.ds {
background: #ffffff;
padding-top: 20px;
}
.ds h4 {
font-size: 14px;
font-weight: 700;
}
.ds h5 {
font-size: 10px;
font-weight: 700;
}
.ds h3 {
color: #ffffff;
font-size: 16px;
padding: 0 10px;
line-height: 60px;
height: 60px;
margin: 0;
background: #ff865c;
text-align: center;
}
.ds i {
font-size: 12px;
line-height: 16px;
}
.ds .desc {
border-bottom: 1px solid #eaeaea;
display: inline-block;
padding: 15px 0;
width: 100%;
}
.ds .desc:hover {
background: #f2f2f2;
}
.ds .thumb {
width: 30px;
margin: 0 10px 0 20px;
display: block;
float: left;
}
.ds .details {
width: 170px;
float: left;
}
.ds > .desc p {
font-size: 11px;
}
.ds p > muted {
font-size: 9px;
text-transform: uppercase;
font-style: italic;
color: #666666
}
.ds a {
color: #FF9C77;
}
/* LINE ICONS CONFIGURATION */
.mtbox {
margin-top: 80px;
margin-bottom: 40px;
}
.box1 {
padding: 15px;
text-align: center;
color: #989898;
border-bottom: 1px solid #989898;
}
.box1 span {
font-size: 50px;
}
.box1 h3 {
text-align: center;
}
.box0:hover .box1 {
border-bottom: 1px solid #ffffff;
}
.box0 p {
text-align: center;
font-size: 12px;
color: #f2f2f2;
}
.box0:hover p {
color: #ff865c;
}
.box0:hover {
background: #ffffff;
box-shadow: 0 2px 1px rgba(0, 0, 0, 0.2);
}
/* *************************************************************************************
PANELS CONFIGURATIONS
*************************************************************************************** */
/*Panel Size*/
.pn {
height: 250px;
box-shadow: 0 2px 1px rgba(0, 0, 0, 0.2);
}
.pn:hover {
box-shadow: 2px 3px 2px rgba(0, 0, 0, 0.3);
}
/*Grey Panel*/
.grey-panel {
text-align: center;
background: #dfdfe1;
}
.grey-panel .grey-header {
background: #ccd1d9;
padding: 3px;
margin-bottom: 15px;
}
.grey-panel h5 {
font-weight: 200;
margin-top: 10px;
}
.grey-panel p {
margin-left: 5px;
}
/* Specific Conf for Donut Charts*/
.donut-chart p {
margin-top: 5px;
font-weight: 700;
margin-left: 15px;
}
.donut-chart h2 {
font-weight: 900;
color: #FF6B6B;
font-size: 38px;
}
/* Dark Blue*/
.darkblue-panel {
text-align: center;
background: #444c57;
}
.darkblue-panel .darkblue-header {
background: transparent;
padding: 3px;
margin-bottom: 15px;
}
.darkblue-panel h1 {
color: #f2f2f2;
}
.darkblue-panel h5 {
font-weight: 200;
margin-top: 10px;
color: white;
}
.darkblue-panel footer {
color: white;
}
.darkblue-panel footer h5 {
margin-left:10px;
margin-right: 10px;
font-weight: 700;
}
/*Green Panel*/
.green-panel {
text-align: center;
background: #68dff0;
}
.green-panel .green-header {
background: #43b1a9;
padding: 3px;
margin-bottom: 15px;
}
.green-panel h5 {
color: white;
font-weight: 200;
margin-top: 10px;
}
.green-panel h3 {
color: white;
font-weight: 100;
}
.green-panel p {
color: white;
}
/*White Panel */
.white-panel {
text-align: center;
background: #ffffff;
color: #ccd1d9;
}
²
.white-panel p {
margin-top: 5px;
font-weight: 700;
margin-left: 15px;
}
.white-panel .white-header {
background: #f4f4f4;
padding: 3px;
margin-bottom: 15px;
color: #c6c6c6;
}
.white-panel .small {
font-size: 10px;
color: #ccd1d9;
}
.white-panel i {
color: #68dff0;
padding-right: 4px;
font-size: 14px;
}
/*Content Panel*/
.content-panel {
background: #ffffff;
box-shadow: 0px 3px 2px #aab2bd;
padding-top: 15px;
padding-bottom: 5px;
}
.content-panel h4 {
margin-left: 10px;
}
.content-panel h5 {
margin-left: 10px;
color:#fff;
}
/* FORMS CONFIGURATION */
.form-panel {
background: #ffffff;
margin: 10px;
padding: 20px;
box-shadow: 0px 3px 2px #aab2bd;
text-align: left;
}
label {
font-weight: 400;
}
.form-horizontal.style-form .form-group {
border-bottom: 1px solid #eff2f7;
padding-bottom: 15px;
margin-bottom: 15px;
}
.round-form {
border-radius: 500px;
-webkit-border-radius: 500px;
}
@media (min-width: 768px) {
.form-horizontal .control-label {
text-align: left;
}
}
#focusedInput {
border: 1px solid #ed5565;
box-shadow: none;
}
.add-on {
float: right;
margin-top: -37px;
padding: 3px;
text-align: center;
}
.add-on .btn {
height: 34px;
}
/* TOGGLE CONFIGURATION */
.has-switch {
border-radius: 30px;
-webkit-border-radius: 30px;
display: inline-block;
cursor: pointer;
line-height: 1.231;
overflow: hidden;
position: relative;
text-align: left;
width: 80px;
-webkit-mask: url('../img/mask.png') 0 0 no-repeat;
mask: url('../img/mask.png') 0 0 no-repeat;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.has-switch.deactivate {
opacity: 0.5;
filter: alpha(opacity=50);
cursor: default !important;
}
.has-switch.deactivate label,
.has-switch.deactivate span {
cursor: default !important;
}
.has-switch > div {
width: 162%;
position: relative;
top: 0;
}
.has-switch > div.switch-animate {
-webkit-transition: left 0.25s ease-out;
-moz-transition: left 0.25s ease-out;
-o-transition: left 0.25s ease-out;
transition: left 0.25s ease-out;
-webkit-backface-visibility: hidden;
}
.has-switch > div.switch-off {
left: -63%;
}
.has-switch > div.switch-off label {
background-color: #7f8c9a;
border-color: #bdc3c7;
-webkit-box-shadow: -1px 0 0 rgba(255, 255, 255, 0.5);
-moz-box-shadow: -1px 0 0 rgba(255, 255, 255, 0.5);
box-shadow: -1px 0 0 rgba(255, 255, 255, 0.5);
}
.has-switch > div.switch-on {
left: 0%;
}
.has-switch > div.switch-on label {
background-color: #41cac0;
}
.has-switch input[type=checkbox] {
display: none;
}
.has-switch span {
cursor: pointer;
font-size: 14.994px;
font-weight: 700;
float: left;
height: 29px;
line-height: 19px;
margin: 0;
padding-bottom: 6px;
padding-top: 5px;
position: relative;
text-align: center;
width: 50%;
z-index: 1;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: 0.25s ease-out;
-moz-transition: 0.25s ease-out;
-o-transition: 0.25s ease-out;
transition: 0.25s ease-out;
-webkit-backface-visibility: hidden;
}
.has-switch span.switch-left {
border-radius: 30px 0 0 30px;
background-color: #2A3542;
color: #41cac0;
border-left: 1px solid transparent;
}
.has-switch span.switch-right {
border-radius: 0 30px 30px 0;
background-color: #bdc3c7;
color: #ffffff;
text-indent: 7px;
}
.has-switch span.switch-right [class*="fui-"] {
text-indent: 0;
}
.has-switch label {
border: 4px solid #2A3542;
border-radius: 50%;
-webkit-border-radius: 50%;
float: left;
height: 29px;
margin: 0 -21px 0 -14px;
padding: 0;
position: relative;
vertical-align: middle;
width: 29px;
z-index: 100;
-webkit-transition: 0.25s ease-out;
-moz-transition: 0.25s ease-out;
-o-transition: 0.25s ease-out;
transition: 0.25s ease-out;
-webkit-backface-visibility: hidden;
}
.switch-square {
border-radius: 6px;
-webkit-border-radius: 6px;
-webkit-mask: url('../img/mask.png') 0 0 no-repeat;
mask: url('../img/mask.png') 0 0 no-repeat;
}
.switch-square > div.switch-off label {
border-color: #7f8c9a;
border-radius: 6px 0 0 6px;
}
.switch-square span.switch-left {
border-radius: 6px 0 0 6px;
}
.switch-square span.switch-left [class*="fui-"] {
text-indent: -10px;
}
.switch-square span.switch-right {
border-radius: 0 6px 6px 0;
}
.switch-square span.switch-right [class*="fui-"] {
text-indent: 5px;
}
.switch-square label {
border-radius: 0 6px 6px 0;
border-color: #41cac0;
}
/*badge*/
.badge.bg-primary {
background: #8075c4;
}
.badge.bg-success {
background: #a9d86e;
}
.badge.bg-warning {
background: #FCB322;
}
.badge.bg-important {
background: #ff6c60;
}
.badge.bg-info {
background: #41cac0;
}
.badge.bg-inverse {
background: #2A3542;
}
/*todolist*/
/*footer*/
.site-footer {
background: #FF9C77;
color: #fff;
padding: 10px 0;
}
.site-min-height {
min-height: 900px;
}

View file

@ -1,7 +0,0 @@
I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project,
Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome,
comprehensive icon sets or copy and paste your own.
Please. Check it out.
-Dave Gandy

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more