├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.txt ├── DESCRIPTION ├── LICENSE ├── MANIFEST.in ├── README.rst ├── calendar_view.png ├── calendarium ├── __init__.py ├── admin.py ├── constants.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── settings.py ├── static │ └── calendarium │ │ ├── css │ │ ├── calendar.css │ │ ├── calendar.less │ │ └── colorpicker.css │ │ ├── img │ │ ├── blank.gif │ │ ├── colorpicker_background.png │ │ ├── colorpicker_hex.png │ │ ├── colorpicker_hsb_b.png │ │ ├── colorpicker_hsb_h.png │ │ ├── colorpicker_hsb_s.png │ │ ├── colorpicker_indic.gif │ │ ├── colorpicker_overlay.png │ │ ├── colorpicker_rgb_b.png │ │ ├── colorpicker_rgb_g.png │ │ ├── colorpicker_rgb_r.png │ │ ├── colorpicker_select.gif │ │ ├── colorpicker_submit.png │ │ ├── custom_background.png │ │ ├── custom_hex.png │ │ ├── custom_hsb_b.png │ │ ├── custom_hsb_h.png │ │ ├── custom_hsb_s.png │ │ ├── custom_indic.gif │ │ ├── custom_rgb_b.png │ │ ├── custom_rgb_g.png │ │ ├── custom_rgb_r.png │ │ ├── custom_submit.png │ │ ├── select.png │ │ ├── select2.png │ │ └── slider.png │ │ └── js │ │ ├── colorpicker.js │ │ ├── colorpicker_list.js │ │ ├── eye.js │ │ ├── layout.js │ │ └── utils.js ├── templates │ ├── 404.html │ ├── base.html │ └── calendarium │ │ ├── calendar_day.html │ │ ├── calendar_month.html │ │ ├── calendar_week.html │ │ ├── event_confirm_delete.html │ │ ├── event_detail.html │ │ ├── event_form.html │ │ ├── occurrence_confirm_delete.html │ │ ├── occurrence_detail.html │ │ ├── occurrence_form.html │ │ ├── partials │ │ ├── calendar_day.html │ │ ├── calendar_month.html │ │ ├── calendar_week.html │ │ ├── category_list.html │ │ └── upcoming_events.html │ │ └── upcoming_events.html ├── templatetags │ ├── __init__.py │ └── calendarium_tags.py ├── tests │ ├── __init__.py │ ├── forms_tests.py │ ├── models_tests.py │ ├── settings.py │ ├── tags_tests.py │ ├── test_app │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ └── south_migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ ├── test_settings.py │ ├── urls.py │ └── views_tests.py ├── urls.py ├── utils.py └── views.py ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── contribute.rst │ ├── index.rst │ ├── installation.rst │ └── usage.rst ├── manage.py ├── requirements.txt ├── runtests.py ├── setup.py ├── test_requirements.txt └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info/ 3 | *.pyc 4 | .ropeproject 5 | .tox 6 | chromedriver.log 7 | .coverage 8 | coverage/ 9 | docs/build/ 10 | db.sqlite 11 | dist/ 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | env: 6 | - DJANGO=Django==1.4.5 7 | - DJANGO=Django==1.5.1 8 | install: pip install $DJANGO --use-mirrors 9 | script: python setup.py test 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Current or previous core committers 2 | 3 | Tobias Lorenz 4 | Daniel Kaufhold 5 | 6 | Contributors (in alphabetical order) 7 | 8 | * Your name could stand here :) 9 | * Bob Bowles (BobBowles) 10 | * Danny Im (minadyn) 11 | * Jay Crossler (jaycrossler) 12 | * Keith (keithhackbarth) 13 | * Trey Hunner (treyhunner) 14 | * atvonk 15 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | === ongoing === 2 | 3 | - Corrected instructions for contributions 4 | - Added some instruction for template tags 5 | - Added some test cases for extra coverage 6 | - Fixed occurrence generation for events with multiple days 7 | 8 | === 1.3.4 === 9 | 10 | - Prepared app for Django1.9 and Python3.5 11 | - Replaced factory_boy with mixer 12 | 13 | === 1.3.3 === 14 | 15 | - fixed AttributeError for EventCategory 16 | 17 | === 1.3.2 === 18 | 19 | - Python3.X related improvements 20 | 21 | === 1.3.1 === 22 | 23 | - Fixed generator issues 24 | 25 | === 1.3 === 26 | 27 | - Prepared app for Django>=1.8 28 | 29 | === 1.2 === 30 | 31 | - Prepared app for Django>=1.7 32 | - Added runtests.sh script 33 | - Updated requirements 34 | 35 | === 1.0 === 36 | 37 | - Added basic bootstrap styles 38 | 39 | === 0.6 === 40 | 41 | - Made use of Django's new user model setting: AUTH_USER_MODEL 42 | - Upgraded to Django>=1.6 43 | 44 | === 0.5 === 45 | 46 | - Updated urls.py to be compatible with Django 1.6 47 | - Documentation improvements 48 | 49 | === 0.4.6 === 50 | 51 | - Added auto-correction of event end date if end date is before start date 52 | 53 | === 0.4.5 === 54 | 55 | - re-enabled deprecated class name for backwards compatibility 56 | 57 | === 0.4.4 === 58 | 59 | - Fixed factory name and tests 60 | - Added related name to image field. 61 | 62 | === 0.4.3 === 63 | 64 | - Added get_parent_category method to Event model. 65 | 66 | === 0.4.2 === 67 | 68 | - Added CategorySlugMixin for views that should filter by category. 69 | - Fixed a bug with the category filtering. 70 | 71 | === 0.4.1 === 72 | 73 | - Taking parent category into consideration when filtering for occurrences 74 | 75 | === 0.4 === 76 | 77 | - Added UpcomingEventsAjaxView 78 | - Added slug and parent fields to EventCategory model 79 | - Added assignment tag `get_upcoming_events` 80 | - Added FilerImageField 81 | 82 | === 0.3 === 83 | 84 | - Made category field on Event model optional (backwards incompatible 85 | migration) 86 | - Improved Event admin 87 | 88 | === 0.2.1 === 89 | 90 | - Added sphinx docs, hosted on readthedocs (https://django-calendarium.readthedocs.org) 91 | 92 | === 0.2 === 93 | 94 | - Added render_upcoming_events template tag. 95 | - Made created_by optional 96 | 97 | === 0.1.1 === 98 | 99 | - Added view to redirect to the current month 100 | 101 | === 0.1 === 102 | 103 | Initial commit 104 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | A reusable Django app to manage and display a calendar in your templates. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2012 Martin Brochhaus 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include DESCRIPTION 4 | include CHANGELOG.txt 5 | include README.md 6 | graft calendarium 7 | graft docs 8 | graft docs/source 9 | global-exclude *.orig *.pyc *.log *.swp 10 | prune docs/build 11 | prune calendarium/tests/coverage 12 | prune calendarium/.ropeproject 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Calendarium 2 | ================== 3 | 4 | A Django application for managing and displaying a calendar and its events. 5 | 6 | Installation 7 | ------------ 8 | 9 | For further information, like requirements, please check out the 10 | [django-calendarium documentation](https://django-calendarium.readthedocs.org/) 11 | on readthedocs. 12 | 13 | .. image:: https://raw.githubusercontent.com/bitmazk/django-calendarium/master/calendar_view.png 14 | :alt: Calendar Example 15 | 16 | 17 | We added some basic bootstrap styles to start with. If you're not using 18 | Bootstrap we recommend to build your own styles, otherwise try:: 19 | 20 | {% load static %} 21 | 22 | 23 | 24 | 25 | Settings 26 | -------- 27 | 28 | If you want your calendar to start on a different date, you can set the 29 | ``CALENDARIUM_SHIFT_WEEKSTART`` setting to be the offset in days, that the 30 | calendar should add or subtract from the start day of the week. Most common 31 | case is probably, that you want your calendar week to start on sunday in which 32 | case you would add the following to your settings:: 33 | 34 | CALENDARIUM_SHIFT_WEEKSTART = -1 35 | 36 | Extending the app 37 | ----------------- 38 | 39 | It is almost inevitable that you will want to add more fields or more 40 | functionality to the Event model of this app. However, this app is already 41 | quite complex and we would like to keep it as simple and focused as possible. 42 | This app should do one thing and do it well, and that thing is: to output 43 | (recurring) events for a given day, week, month or timeframe. 44 | 45 | A very common usecase is to display public events that are open for 46 | registration. For this case we have created another app [django-event-rsvp](https://github.com/bitmazk/django-event-rsvp) which plays nicely with this app. 47 | 48 | You might do it in a similar way. Since events created in the calendarium app 49 | can easily be tied to any object via generic foreign keys, you can therefore 50 | tie them to the objects of any of your own apps. The only thing left for you is 51 | to create nice CRUD views that create your own objects and our Event objects 52 | simultaneously behind the scenes. 53 | 54 | 55 | Roadmap 56 | ------- 57 | 58 | Check the issue tracker on github for milestones and features to come. If you 59 | have ideas or questions, please don't hesitate to open an issue on the issue 60 | tracker. 61 | 62 | Compatibility 63 | ------------- 64 | 65 | +-------+-------+-------+-------+-------+-------+ 66 | |py\dj | 1.5 | 1.6 | 1.7 | 1.8 | 1.9 | 67 | +=======+=======+=======+=======+=======+=======+ 68 | |2.6 | X | X | X | X | X | 69 | +-------+-------+-------+-------+-------+-------+ 70 | |2.7 | X | PASS | PASS | PASS | PASS | 71 | +-------+-------+-------+-------+-------+-------+ 72 | |3.2 | X | PASS | PASS | PASS | PASS | 73 | +-------+-------+-------+-------+-------+-------+ 74 | |3.5 | PASS | PASS | PASS | PASS | PASS | 75 | +-------+-------+-------+-------+-------+-------+ 76 | -------------------------------------------------------------------------------- /calendar_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendar_view.png -------------------------------------------------------------------------------- /calendarium/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '1.3.4' 3 | -------------------------------------------------------------------------------- /calendarium/admin.py: -------------------------------------------------------------------------------- 1 | """Admin views for the models of the ``calendarium`` app.""" 2 | from django.contrib import admin 3 | 4 | from calendarium.models import ( 5 | Event, 6 | EventCategory, 7 | EventRelation, 8 | Occurrence, 9 | Rule, 10 | ) 11 | 12 | 13 | class EventAdmin(admin.ModelAdmin): 14 | """Custom admin for the ``Event`` model.""" 15 | model = Event 16 | fields = ( 17 | 'title', 'start', 'end', 'description', 'category', 'created_by', 18 | 'rule', 'end_recurring_period', ) 19 | list_display = ( 20 | 'title', 'start', 'end', 'category', 'created_by', 'rule', 21 | 'end_recurring_period', ) 22 | search_fields = ('title', 'description', ) 23 | date_hierarchy = 'start' 24 | list_filter = ('category', ) 25 | 26 | 27 | class EventCategoryAdmin(admin.ModelAdmin): 28 | """Custom admin to display a small colored square.""" 29 | model = EventCategory 30 | list_display = ('name', 'color', ) 31 | list_editable = ('color', ) 32 | 33 | 34 | admin.site.register(Event, EventAdmin) 35 | admin.site.register(EventCategory, EventCategoryAdmin) 36 | admin.site.register(EventRelation) 37 | admin.site.register(Occurrence) 38 | admin.site.register(Rule) 39 | -------------------------------------------------------------------------------- /calendarium/constants.py: -------------------------------------------------------------------------------- 1 | """Constants for the ``calendarium`` app.""" 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | FREQUENCIES = { 6 | 'YEARLY': 'YEARLY', 7 | 'MONTHLY': 'MONTHLY', 8 | 'WEEKLY': 'WEEKLY', 9 | 'DAILY': 'DAILY', 10 | } 11 | 12 | 13 | FREQUENCY_CHOICES = ( 14 | (FREQUENCIES['YEARLY'], _('Yearly')), 15 | (FREQUENCIES['MONTHLY'], _('Monthly')), 16 | (FREQUENCIES['WEEKLY'], _('Weekly')), 17 | (FREQUENCIES['DAILY'], _('Daily')), 18 | ) 19 | 20 | 21 | OCCURRENCE_DECISIONS = { 22 | 'all': 'all', 23 | 'following': 'following', 24 | 'this one': 'this one', 25 | } 26 | 27 | OCCURRENCE_DECISION_CHOICESS = ( 28 | (OCCURRENCE_DECISIONS['all'], _('all')), 29 | (OCCURRENCE_DECISIONS['following'], _('following')), 30 | (OCCURRENCE_DECISIONS['this one'], _('this one')), 31 | ) 32 | -------------------------------------------------------------------------------- /calendarium/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for the ``calendarium`` app.""" 2 | from django import forms 3 | from django.contrib.auth.models import User 4 | from django.forms.models import model_to_dict 5 | from django.utils.timezone import datetime, timedelta 6 | 7 | from .constants import OCCURRENCE_DECISION_CHOICESS, OCCURRENCE_DECISIONS 8 | from .models import Event, Occurrence 9 | 10 | 11 | class OccurrenceForm(forms.ModelForm): 12 | """A form for the ``Occurrence`` model.""" 13 | decision = forms.CharField( 14 | widget=forms.Select(choices=OCCURRENCE_DECISION_CHOICESS), 15 | ) 16 | 17 | cancelled = forms.BooleanField( 18 | widget=forms.HiddenInput, 19 | required=False, 20 | ) 21 | 22 | original_start = forms.DateTimeField( 23 | widget=forms.HiddenInput, 24 | ) 25 | 26 | original_end = forms.DateTimeField( 27 | widget=forms.HiddenInput, 28 | ) 29 | 30 | event = forms.ModelChoiceField( 31 | widget=forms.HiddenInput, 32 | queryset=Event.objects.all(), 33 | ) 34 | 35 | class Meta: 36 | model = Occurrence 37 | exclude = [] 38 | 39 | def save(self): 40 | cleaned_data = self.cleaned_data 41 | if cleaned_data['decision'] == OCCURRENCE_DECISIONS['all']: 42 | changes = dict( 43 | (key, value) for key, value in iter(cleaned_data.items()) 44 | if value != self.initial.get(key) and self.initial.get(key)) 45 | event = self.instance.event 46 | # for each field on the event, check for new data in cleaned_data 47 | for field_name in [field.name for field in event._meta.fields]: 48 | value = changes.get(field_name) 49 | if value: 50 | setattr(event, field_name, value) 51 | event.save() 52 | 53 | # repeat for persistent occurrences 54 | for occ in event.occurrences.all(): 55 | for field_name in [field.name for field in occ._meta.fields]: 56 | value = changes.get(field_name) 57 | if value: 58 | # since we can't just set a new datetime, we have to 59 | # adjust the datetime fields according to the changes 60 | # on the occurrence form instance 61 | if type(value) != datetime: 62 | setattr(occ, field_name, value) 63 | else: 64 | initial_time = self.initial.get(field_name) 65 | occ_time = getattr(occ, field_name) 66 | delta = value - initial_time 67 | new_time = occ_time + delta 68 | setattr(occ, field_name, new_time) 69 | occ.save() 70 | 71 | # get everything from initial and compare to cleaned_data to 72 | # retrieve what has been changed 73 | # apply those changes to the persistent occurrences (and the main 74 | # event) 75 | elif cleaned_data['decision'] == OCCURRENCE_DECISIONS['this one']: 76 | self.instance.save() 77 | elif cleaned_data['decision'] == OCCURRENCE_DECISIONS['following']: 78 | # get the changes 79 | changes = dict( 80 | (key, value) for key, value in iter(cleaned_data.items()) 81 | if value != self.initial.get(key) and self.initial.get(key)) 82 | 83 | # change the old event 84 | old_event = self.instance.event 85 | end_recurring_period = self.instance.event.end_recurring_period 86 | old_event.end_recurring_period = self.instance.start - timedelta( 87 | days=1) 88 | old_event.save() 89 | 90 | # the instance occurrence holds the info for the new event, that we 91 | # use to update the old event's fields 92 | new_event = old_event 93 | new_event.end_recurring_period = end_recurring_period 94 | new_event.id = None 95 | event_kwargs = model_to_dict(self.instance) 96 | for field_name in [field.name for field in new_event._meta.fields]: 97 | if (field_name == 'created_by' and 98 | event_kwargs.get('created_by')): 99 | value = User.objects.get(pk=event_kwargs.get(field_name)) 100 | elif field_name in ['rule', 'category']: 101 | continue 102 | else: 103 | value = event_kwargs.get(field_name) 104 | if value: 105 | setattr(new_event, field_name, value) 106 | new_event.save() 107 | -------------------------------------------------------------------------------- /calendarium/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-02 13:28 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django_libs.models 7 | import filer.fields.image 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ('contenttypes', '0002_remove_content_type_name'), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='Event', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('start', models.DateTimeField(verbose_name='Start date')), 26 | ('end', models.DateTimeField(verbose_name='End date')), 27 | ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), 28 | ('description', models.TextField(blank=True, max_length=2048, verbose_name='Description')), 29 | ('end_recurring_period', models.DateTimeField(blank=True, null=True, verbose_name='End of recurring')), 30 | ('title', models.CharField(max_length=256, verbose_name='Title')), 31 | ], 32 | options={ 33 | 'abstract': False, 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='Rule', 38 | fields=[ 39 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('name', models.CharField(max_length=32, verbose_name='name')), 41 | ('description', models.TextField(verbose_name='description')), 42 | ('frequency', models.CharField(choices=[('YEARLY', 'Yearly'), ('MONTHLY', 'Monthly'), ('WEEKLY', 'Weekly'), ('DAILY', 'Daily')], max_length=10, verbose_name='frequency')), 43 | ('params', models.TextField(blank=True, null=True, verbose_name='params')), 44 | ], 45 | ), 46 | migrations.CreateModel( 47 | name='Occurrence', 48 | fields=[ 49 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 50 | ('start', models.DateTimeField(verbose_name='Start date')), 51 | ('end', models.DateTimeField(verbose_name='End date')), 52 | ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), 53 | ('description', models.TextField(blank=True, max_length=2048, verbose_name='Description')), 54 | ('original_start', models.DateTimeField(verbose_name='Original start')), 55 | ('original_end', models.DateTimeField(verbose_name='Original end')), 56 | ('cancelled', models.BooleanField(default=False, verbose_name='Cancelled')), 57 | ('title', models.CharField(blank=True, max_length=256, verbose_name='Title')), 58 | ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='occurrences', to=settings.AUTH_USER_MODEL, verbose_name='Created by')), 59 | ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='occurrences', to='calendarium.event', verbose_name='Event')), 60 | ], 61 | options={ 62 | 'abstract': False, 63 | }, 64 | ), 65 | migrations.CreateModel( 66 | name='EventRelation', 67 | fields=[ 68 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 69 | ('object_id', models.IntegerField()), 70 | ('relation_type', models.CharField(blank=True, max_length=32, null=True, verbose_name='Relation type')), 71 | ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), 72 | ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='calendarium.event', verbose_name='Event')), 73 | ], 74 | ), 75 | migrations.CreateModel( 76 | name='EventCategory', 77 | fields=[ 78 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 79 | ('name', models.CharField(max_length=256, verbose_name='Name')), 80 | ('slug', models.SlugField(blank=True, max_length=256, verbose_name='Slug')), 81 | ('color', django_libs.models.ColorField(max_length=6, verbose_name='Color')), 82 | ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parents', to='calendarium.eventcategory', verbose_name='Parent')), 83 | ], 84 | ), 85 | migrations.AddField( 86 | model_name='event', 87 | name='category', 88 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='events', to='calendarium.eventcategory', verbose_name='Category'), 89 | ), 90 | migrations.AddField( 91 | model_name='event', 92 | name='created_by', 93 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='events', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), 94 | ), 95 | migrations.AddField( 96 | model_name='event', 97 | name='image', 98 | field=filer.fields.image.FilerImageField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='calendarium_event_images', to=settings.FILER_IMAGE_MODEL, verbose_name='Image'), 99 | ), 100 | migrations.AddField( 101 | model_name='event', 102 | name='rule', 103 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='calendarium.rule', verbose_name='Rule'), 104 | ), 105 | ] 106 | -------------------------------------------------------------------------------- /calendarium/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/migrations/__init__.py -------------------------------------------------------------------------------- /calendarium/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Models for the ``calendarium`` app. 3 | 4 | The code of these models is highly influenced by or taken from the models of 5 | django-schedule: 6 | 7 | https://github.com/thauber/django-schedule/tree/master/schedule/models 8 | 9 | """ 10 | import json 11 | 12 | from django.conf import settings 13 | from django.contrib.contenttypes.fields import GenericForeignKey 14 | from django.contrib.contenttypes.models import ContentType 15 | from django.urls import reverse 16 | from django.db import models 17 | from django.db.models import Q 18 | from django.template.defaultfilters import slugify 19 | from django.utils.timezone import timedelta 20 | from django.utils.translation import ugettext_lazy as _ 21 | 22 | from dateutil import rrule 23 | from django_libs.models import ColorField 24 | from filer.fields.image import FilerImageField 25 | 26 | from .constants import FREQUENCY_CHOICES, OCCURRENCE_DECISIONS, FREQUENCIES 27 | from .utils import OccurrenceReplacer 28 | 29 | 30 | class EventModelManager(models.Manager): 31 | """Custom manager for the ``Event`` model class.""" 32 | def get_occurrences(self, start, end, category=None): 33 | """Returns a list of events and occurrences for the given period.""" 34 | # we always want the time of start and end to be at 00:00 35 | start = start.replace(minute=0, hour=0) 36 | end = end.replace(minute=0, hour=0) 37 | # if we recieve the date of one day as start and end, we need to set 38 | # end one day forward 39 | if start == end: 40 | end = start + timedelta(days=1) 41 | # retrieving relevant events 42 | # TODO currently for events with a rule, I can't properly find out when 43 | # the last occurrence of the event ends, or find a way to filter that, 44 | # so I'm still fetching **all** events before this period, that have a 45 | # end_recurring_period. 46 | # For events without a rule, I fetch only the relevant ones. 47 | 48 | # Django < 1.6 compatibility 49 | getQuerySet = (self.get_query_set if hasattr( 50 | self, 'get_query_set') else self.get_queryset) 51 | qs = getQuerySet() 52 | 53 | if category: 54 | qs = qs.filter(start__lt=end) 55 | relevant_events = qs.filter( 56 | Q(category=category) | 57 | Q(category__parent=category) 58 | ) 59 | else: 60 | relevant_events = qs.filter(start__lt=end) 61 | # get all occurrences for those events that don't already have a 62 | # persistent match and that lie in this period. 63 | all_occurrences = [] 64 | for event in relevant_events: 65 | all_occurrences.extend(event.get_occurrences(start, end)) 66 | 67 | # sort and return 68 | return sorted(all_occurrences, key=lambda x: x.start) 69 | 70 | 71 | class EventModelMixin(models.Model): 72 | """ 73 | Abstract base class to prevent code duplication. 74 | :start: The start date of the event. 75 | :end: The end date of the event. 76 | :creation_date: When this event was created. 77 | :description: The description of the event. 78 | 79 | """ 80 | start = models.DateTimeField( 81 | verbose_name=_('Start date'), 82 | ) 83 | 84 | end = models.DateTimeField( 85 | verbose_name=_('End date'), 86 | ) 87 | 88 | creation_date = models.DateTimeField( 89 | verbose_name=_('Creation date'), 90 | auto_now_add=True, 91 | ) 92 | 93 | description = models.TextField( 94 | max_length=2048, 95 | verbose_name=_('Description'), 96 | blank=True, 97 | ) 98 | 99 | def __str__(self): 100 | return self.title 101 | 102 | def save(self, *args, **kwargs): 103 | # start should override end if end is set wrong. This auto-corrects 104 | # usage errors when creating or updating events. 105 | if self.end < self.start: 106 | self.end = self.start 107 | return super(EventModelMixin, self).save(*args, **kwargs) 108 | 109 | class Meta: 110 | abstract = True 111 | 112 | 113 | class Event(EventModelMixin): 114 | """ 115 | Hold the information about an event in the calendar. 116 | 117 | :created_by: FK to the ``User``, who created this event. 118 | :category: FK to the ``EventCategory`` this event belongs to. 119 | :rule: FK to the definition of the recurrence of an event. 120 | :end_recurring_period: The possible end of the recurring definition. 121 | :title: The title of the event. 122 | :image: Optional image of the event. 123 | 124 | """ 125 | 126 | created_by = models.ForeignKey( 127 | settings.AUTH_USER_MODEL, 128 | verbose_name=_('Created by'), 129 | related_name='events', 130 | blank=True, null=True, 131 | on_delete=models.SET_NULL, 132 | ) 133 | 134 | category = models.ForeignKey( 135 | 'EventCategory', 136 | verbose_name=_('Category'), 137 | related_name='events', 138 | null=True, blank=True, 139 | on_delete=models.SET_NULL, 140 | ) 141 | 142 | rule = models.ForeignKey( 143 | 'Rule', 144 | verbose_name=_('Rule'), 145 | blank=True, null=True, 146 | on_delete=models.SET_NULL, 147 | ) 148 | 149 | end_recurring_period = models.DateTimeField( 150 | verbose_name=_('End of recurring'), 151 | blank=True, null=True, 152 | ) 153 | 154 | title = models.CharField( 155 | max_length=256, 156 | verbose_name=_('Title'), 157 | ) 158 | 159 | image = FilerImageField( 160 | verbose_name=_('Image'), 161 | related_name='calendarium_event_images', 162 | null=True, blank=True, 163 | on_delete=models.SET_NULL, 164 | ) 165 | 166 | objects = EventModelManager() 167 | 168 | def get_absolute_url(self): 169 | return reverse('calendar_event_detail', kwargs={'pk': self.pk}) 170 | 171 | def _create_occurrence(self, occ_start, occ_end=None): 172 | """Creates an Occurrence instance.""" 173 | # if the length is not altered, it is okay to only pass occ_start 174 | if not occ_end: 175 | occ_end = occ_start + (self.end - self.start) 176 | return Occurrence( 177 | event=self, start=occ_start, end=occ_end, 178 | # TODO not sure why original start and end also are occ_start/_end 179 | original_start=occ_start, original_end=occ_end, 180 | title=self.title, description=self.description, 181 | creation_date=self.creation_date, created_by=self.created_by) 182 | 183 | def _get_date_gen(self, rr, start, end): 184 | """Returns a generator to create the start dates for occurrences.""" 185 | date = rr.after(start) 186 | while end and date <= end or not(end): 187 | yield date 188 | date = rr.after(date) 189 | 190 | def _get_occurrence_gen(self, start, end): 191 | """Computes all occurrences for this event from start to end.""" 192 | # get length of the event 193 | length = self.end - self.start 194 | 195 | if self.rule: 196 | # if the end of the recurring period is before the end arg passed 197 | # the end of the recurring period should be the new end 198 | if self.end_recurring_period and end and ( 199 | self.end_recurring_period < end): 200 | end = self.end_recurring_period 201 | # making start date generator 202 | occ_start_gen = self._get_date_gen( 203 | self.get_rrule_object(), 204 | start - length, end) 205 | 206 | # choosing the first item from the generator to initiate 207 | occ_start = next(occ_start_gen) 208 | while not end or (end and occ_start <= end): 209 | occ_end = occ_start + length 210 | yield self._create_occurrence(occ_start, occ_end) 211 | occ_start = next(occ_start_gen) 212 | else: 213 | # check if event is in the period 214 | if (not end or self.start < end) and self.end >= start: 215 | # making start date generator 216 | occ_start_gen = self._get_date_gen( 217 | rrule.rrule(eval('rrule.{}'.format(FREQUENCIES['DAILY'])), 218 | dtstart=self.start), 219 | start - length, self.end) 220 | 221 | # choosing the first item from the generator to initiate 222 | try: 223 | occ_start = next(occ_start_gen) 224 | while not end or (end and occ_start <= end): 225 | occ_end = occ_start + length 226 | yield self._create_occurrence(occ_start, occ_end) 227 | occ_start = next(occ_start_gen) 228 | except StopIteration: 229 | pass 230 | 231 | def get_occurrences(self, start, end=None): 232 | """Returns all occurrences from start to end.""" 233 | # get persistent occurrences 234 | persistent_occurrences = self.occurrences.all() 235 | 236 | # setup occ_replacer with p_occs 237 | occ_replacer = OccurrenceReplacer(persistent_occurrences) 238 | 239 | # compute own occurrences according to rule that overlap with the 240 | # period 241 | occurrence_gen = self._get_occurrence_gen(start, end) 242 | # get additional occs, that we need to take into concern 243 | additional_occs = occ_replacer.get_additional_occurrences( 244 | start, end) 245 | occ = next(occurrence_gen) 246 | while not end or (occ.start < end or any(additional_occs)): 247 | if occ_replacer.has_occurrence(occ): 248 | p_occ = occ_replacer.get_occurrence(occ) 249 | 250 | # if the persistent occ falls into the period, replace it 251 | if (end and p_occ.start < end) and p_occ.end >= start: 252 | estimated_occ = p_occ 253 | else: 254 | # if there is no persistent match, use the original occ 255 | estimated_occ = occ 256 | 257 | if any(additional_occs) and ( 258 | estimated_occ.start == additional_occs[0].start): 259 | final_occ = additional_occs.pop(0) 260 | else: 261 | final_occ = estimated_occ 262 | if not final_occ.cancelled: 263 | yield final_occ 264 | try: 265 | occ = next(occurrence_gen) 266 | except StopIteration: 267 | break 268 | 269 | def get_parent_category(self): 270 | """Returns the main category of this event.""" 271 | if self.category.parent: 272 | return self.category.parent 273 | return self.category 274 | 275 | def get_rrule_object(self): 276 | """Returns the rrule object for this ``Event``.""" 277 | if self.rule: 278 | params = self.rule.get_params() 279 | frequency = 'rrule.{0}'.format(self.rule.frequency) 280 | return rrule.rrule(eval(frequency), dtstart=self.start, **params) 281 | 282 | 283 | class EventCategory(models.Model): 284 | """ 285 | The category of an event. 286 | 287 | :name: The name of the category. 288 | :slug: The slug of the category. 289 | :color: The color of the category. 290 | :parent: Allows you to create hierarchies of event categories. 291 | 292 | """ 293 | name = models.CharField( 294 | max_length=256, 295 | verbose_name=_('Name'), 296 | ) 297 | 298 | slug = models.SlugField( 299 | max_length=256, 300 | verbose_name=_('Slug'), 301 | blank=True, 302 | ) 303 | 304 | color = ColorField( 305 | verbose_name=_('Color'), 306 | ) 307 | 308 | parent = models.ForeignKey( 309 | 'calendarium.EventCategory', 310 | verbose_name=_('Parent'), 311 | related_name='parents', 312 | null=True, blank=True, 313 | on_delete=models.SET_NULL, 314 | ) 315 | 316 | def __str__(self): 317 | return self.name 318 | 319 | def save(self, *args, **kwargs): 320 | if not self.slug: 321 | self.slug = slugify(self.name) 322 | return super(EventCategory, self).save(*args, **kwargs) 323 | 324 | 325 | class EventRelation(models.Model): 326 | """ 327 | This class allows to relate additional or external data to an event. 328 | 329 | :event: A FK to the ``Event`` this additional data is related to. 330 | :content_type: A FK to ContentType of the generic object. 331 | :object_id: The id of the generic object. 332 | :content_object: The generic foreign key to the generic object. 333 | :relation_type: A string representing the type of the relation. This allows 334 | to relate to the same content_type several times but mean different 335 | things, such as (normal_guests, speakers, keynote_speakers, all being 336 | Guest instances) 337 | 338 | """ 339 | 340 | event = models.ForeignKey( 341 | 'Event', 342 | verbose_name=_("Event"), 343 | on_delete=models.CASCADE, 344 | ) 345 | 346 | content_type = models.ForeignKey( 347 | ContentType, 348 | on_delete=models.CASCADE, 349 | ) 350 | 351 | object_id = models.IntegerField() 352 | 353 | content_object = GenericForeignKey( 354 | 'content_type', 355 | 'object_id', 356 | ) 357 | 358 | relation_type = models.CharField( 359 | verbose_name=_('Relation type'), 360 | max_length=32, 361 | blank=True, null=True, 362 | ) 363 | 364 | def __str__(self): 365 | return u'type "{0}" for "{1}"'.format( 366 | self.relation_type, self.event.title) 367 | 368 | 369 | class Occurrence(EventModelMixin): 370 | """ 371 | Needed if one occurrence of an event has slightly different settings than 372 | all other. 373 | 374 | :created_by: FK to the ``User``, who created this event. 375 | :event: FK to the ``Event`` this ``Occurrence`` belongs to. 376 | :original_start: The original start of the related ``Event``. 377 | :original_end: The original end of the related ``Event``. 378 | :cancelled: True or false of the occurrence's cancellation status. 379 | :title: The title of the event. 380 | 381 | """ 382 | created_by = models.ForeignKey( 383 | settings.AUTH_USER_MODEL, 384 | verbose_name=_('Created by'), 385 | related_name='occurrences', 386 | blank=True, null=True, 387 | on_delete=models.SET_NULL, 388 | ) 389 | 390 | event = models.ForeignKey( 391 | 'Event', 392 | verbose_name=_('Event'), 393 | related_name='occurrences', 394 | on_delete=models.CASCADE, 395 | ) 396 | 397 | original_start = models.DateTimeField( 398 | verbose_name=_('Original start'), 399 | ) 400 | 401 | original_end = models.DateTimeField( 402 | verbose_name=_('Original end'), 403 | ) 404 | 405 | cancelled = models.BooleanField( 406 | verbose_name=_('Cancelled'), 407 | default=False, 408 | ) 409 | 410 | title = models.CharField( 411 | max_length=256, 412 | verbose_name=_('Title'), 413 | blank=True, 414 | ) 415 | 416 | def category(self): 417 | return self.event.category 418 | 419 | def delete_period(self, period): 420 | """Deletes a set of occurrences based on the given decision.""" 421 | # check if this is the last or only one 422 | is_last = False 423 | is_only = False 424 | gen = self.event.get_occurrences( 425 | self.start, self.event.end_recurring_period) 426 | occs = list(set([occ.pk for occ in gen])) 427 | if len(occs) == 1: 428 | is_only = True 429 | elif len(occs) > 1 and self.pk == occs[-1]: 430 | is_last = True 431 | if period == OCCURRENCE_DECISIONS['all']: 432 | # delete all persistent occurrences along with the parent event 433 | self.event.occurrences.all().delete() 434 | self.event.delete() 435 | elif period == OCCURRENCE_DECISIONS['this one']: 436 | # check if it is the last one. If so, shorten the recurring period, 437 | # otherwise cancel the event 438 | if is_last: 439 | self.event.end_recurring_period = self.start - timedelta( 440 | days=1) 441 | self.event.save() 442 | elif is_only: 443 | self.event.occurrences.all().delete() 444 | self.event.delete() 445 | else: 446 | self.cancelled = True 447 | self.save() 448 | elif period == OCCURRENCE_DECISIONS['following']: 449 | # just shorten the recurring period 450 | self.event.end_recurring_period = self.start - timedelta(days=1) 451 | self.event.occurrences.filter(start__gte=self.start).delete() 452 | if is_only: 453 | self.event.delete() 454 | else: 455 | self.event.save() 456 | 457 | def get_absolute_url(self): 458 | return reverse( 459 | 'calendar_occurrence_detail', kwargs={ 460 | 'pk': self.event.pk, 'year': self.start.year, 461 | 'month': self.start.month, 'day': self.start.day}) 462 | 463 | 464 | class Rule(models.Model): 465 | """ 466 | This defines the rule by which an event will recur. 467 | 468 | :name: Name of this rule. 469 | :description: Description of this rule. 470 | :frequency: A string representing the frequency of the recurrence. 471 | :params: JSON string to hold the exact rule parameters as used by 472 | dateutil.rrule to define the pattern of the recurrence. 473 | 474 | """ 475 | name = models.CharField( 476 | verbose_name=_("name"), 477 | max_length=32, 478 | ) 479 | 480 | description = models.TextField( 481 | _("description"), 482 | ) 483 | 484 | frequency = models.CharField( 485 | verbose_name=_("frequency"), 486 | choices=FREQUENCY_CHOICES, 487 | max_length=10, 488 | ) 489 | 490 | params = models.TextField( 491 | verbose_name=_("params"), 492 | blank=True, null=True, 493 | ) 494 | 495 | def __str__(self): 496 | return self.name 497 | 498 | def get_params(self): 499 | if self.params: 500 | return json.loads(self.params) 501 | return {} 502 | -------------------------------------------------------------------------------- /calendarium/settings.py: -------------------------------------------------------------------------------- 1 | """Default settings for the calendarium app.""" 2 | from django.conf import settings 3 | 4 | 5 | SHIFT_WEEKSTART = getattr(settings, 'CALENDARIUM_SHIFT_WEEKSTART', 0) 6 | -------------------------------------------------------------------------------- /calendarium/static/calendarium/css/calendar.css: -------------------------------------------------------------------------------- 1 | #calendar-month, 2 | #calendar-week { 3 | margin-top: 30px; 4 | border: none; 5 | } 6 | #calendar-month tr th, 7 | #calendar-week tr th, 8 | #calendar-month tr td, 9 | #calendar-week tr td { 10 | width: 12.5%; 11 | /* 100% / 8 */ 12 | 13 | width: -moz-calc(12.5%); 14 | width: -webkit-calc(12.5%); 15 | width: calc(12.5%); 16 | } 17 | #calendar-month tr th, 18 | #calendar-week tr th { 19 | border: none; 20 | } 21 | #calendar-month tr td:last-child, 22 | #calendar-week tr td:last-child { 23 | border-right: 1px solid #d5d5d5; 24 | } 25 | #calendar-month tr td.calendarium-empty, 26 | #calendar-week tr td.calendarium-empty, 27 | #calendar-month tr td.calendarium-week-link, 28 | #calendar-week tr td.calendarium-week-link { 29 | background: none; 30 | border: none; 31 | } 32 | #calendar-month tr td.calendarium-current, 33 | #calendar-week tr td.calendarium-current { 34 | background: #f1fcc6; 35 | } 36 | #calendar-month tr td, 37 | #calendar-week tr td { 38 | border-left: 1px solid #d5d5d5; 39 | border-right: none; 40 | line-height: 12px; 41 | border-top: 1px solid #d5d5d5; 42 | border-bottom: none; 43 | padding: 0; 44 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIwIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNlZmVmZWYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); 45 | background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 50%, #f8f8f8 100%); 46 | background: -webkit-gradient(linear, left top, left bottom, color-stop(50%, rgba(255, 255, 255, 0)), color-stop(100%, #f8f8f8)); 47 | background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 50%, #f8f8f8 100%); 48 | background: -o-linear-gradient(top, rgba(255, 255, 255, 0) 50%, #f8f8f8 100%); 49 | background: -ms-linear-gradient(top, rgba(255, 255, 255, 0) 50%, #f8f8f8 100%); 50 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 50%, #f8f8f8 100%); 51 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#efefef', GradientType=0); 52 | } 53 | #calendar-month tr td .calendarium-relative, 54 | #calendar-week tr td .calendarium-relative { 55 | position: relative; 56 | height: 70px; 57 | padding: 8px; 58 | } 59 | #calendar-month tr td .calendarium-date, 60 | #calendar-week tr td .calendarium-date, 61 | #calendar-month tr td .calendarium-day-name, 62 | #calendar-week tr td .calendarium-day-name { 63 | font-size: 10px; 64 | position: absolute; 65 | top: 5px; 66 | right: 5px; 67 | color: #ccc; 68 | } 69 | #calendar-month tr td .calendarium-day-name, 70 | #calendar-week tr td .calendarium-day-name { 71 | display: none; 72 | } 73 | #calendar-month tr td .showAllEvents, 74 | #calendar-week tr td .showAllEvents, 75 | #calendar-month tr td .showAllEvents:hover, 76 | #calendar-week tr td .showAllEvents:hover { 77 | text-decoration: none; 78 | display: none; 79 | } 80 | #calendar-month tr td .showAllEvents i, 81 | #calendar-week tr td .showAllEvents i, 82 | #calendar-month tr td .showAllEvents:hover i, 83 | #calendar-week tr td .showAllEvents:hover i { 84 | margin-right: 2px; 85 | } 86 | #calendar-month tr td .alert, 87 | #calendar-week tr td .alert { 88 | padding: 2px 4px; 89 | margin-bottom: 2px; 90 | margin-right: 17px; 91 | height: 12px; 92 | overflow: hidden; 93 | } 94 | #calendar-month tr td .alert a, 95 | #calendar-week tr td .alert a, 96 | #calendar-month tr td .alert a:hover, 97 | #calendar-week tr td .alert a:hover { 98 | font-size: 10px; 99 | font-weight: normal; 100 | text-decoration: none; 101 | white-space: nowrap; 102 | color: #333; 103 | border: none; 104 | text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.4); 105 | } 106 | #calendar-month tr td .alert .tooltip, 107 | #calendar-week tr td .alert .tooltip { 108 | text-shadow: none; 109 | font-weight: normal; 110 | white-space: nowrap; 111 | } 112 | #calendar-month tr td .alert .tooltip .tooltip-inner, 113 | #calendar-week tr td .alert .tooltip .tooltip-inner { 114 | max-width: 400px; 115 | } 116 | #calendar-month tr:last-child td:nth-child(2), 117 | #calendar-week tr:last-child td:nth-child(2) { 118 | border-left: 1px solid #d5d5d5; 119 | } 120 | #calendar-month tr:last-child td, 121 | #calendar-week tr:last-child td { 122 | border-right: 1px solid #d5d5d5; 123 | border-left: none; 124 | border-bottom: 1px solid #d5d5d5; 125 | } 126 | #calendar-month tr:last-child td.calendarium-empty, 127 | #calendar-week tr:last-child td.calendarium-empty { 128 | border-top: 1px solid #d5d5d5; 129 | border-bottom: none; 130 | border-left: none; 131 | border-right: none; 132 | } 133 | #calendar-month tr:last-child td.calendarium-week-link, 134 | #calendar-week tr:last-child td.calendarium-week-link { 135 | border: none; 136 | } 137 | @media (max-width: 680px) { 138 | #calendar-month, 139 | #calendar-week { 140 | margin-top: 0px; 141 | } 142 | #calendar-month th, 143 | #calendar-week th { 144 | display: none; 145 | } 146 | #calendar-month tr td, 147 | #calendar-week tr td { 148 | display: block; 149 | width: auto; 150 | border-right: 1px solid #eee; 151 | } 152 | #calendar-month tr td.calendarium-empty, 153 | #calendar-week tr td.calendarium-empty { 154 | display: none; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /calendarium/static/calendarium/css/calendar.less: -------------------------------------------------------------------------------- 1 | @black: #000; 2 | @calendar-border: darken(#eee, 10%); 3 | @green: #94ba09; 4 | @white: #fff; 5 | 6 | #calendar-month, #calendar-week { 7 | margin-top: 30px; 8 | border: none; 9 | tr { 10 | th, td { 11 | width: 12.5%; /* 100% / 8 */ 12 | width: -moz-calc(100%/8); 13 | width: -webkit-calc(100%/8); 14 | width: calc(100%/8); 15 | } 16 | th { 17 | border: none; 18 | } 19 | td:last-child { 20 | border-right: 1px solid @calendar-border; 21 | } 22 | td.calendarium-empty, td.calendarium-week-link { 23 | background: none; 24 | border: none; 25 | } 26 | td.calendarium-current { 27 | background: lighten(@green, 50%); 28 | } 29 | td { 30 | border-left: 1px solid @calendar-border; 31 | border-right: none; 32 | line-height: 12px; 33 | border-top: 1px solid @calendar-border; 34 | border-bottom: none; 35 | padding: 0; 36 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIwIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNlZmVmZWYiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); 37 | background: -moz-linear-gradient(top, rgba(255,255,255,0) 50%, rgba(248,248,248,1) 100%); 38 | background: -webkit-gradient(linear, left top, left bottom, color-stop(50%,rgba(255,255,255,0)), color-stop(100%,rgba(248,248,248,1))); 39 | background: -webkit-linear-gradient(top, rgba(255,255,255,0) 50%,rgba(248,248,248,1) 100%); 40 | background: -o-linear-gradient(top, rgba(255,255,255,0) 50%,rgba(248,248,248,1) 100%); 41 | background: -ms-linear-gradient(top, rgba(255,255,255,0) 50%,rgba(248,248,248,1) 100%); 42 | background: linear-gradient(to bottom, rgba(255,255,255,0) 50%,rgba(248,248,248,1) 100%); 43 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#efefef',GradientType=0 ); 44 | .calendarium-relative { 45 | position: relative; 46 | height: 70px; 47 | padding: 8px; 48 | } 49 | .calendarium-date, .calendarium-day-name { 50 | font-size: 10px; 51 | position: absolute; 52 | top: 5px; 53 | right: 5px; 54 | color: #ccc; 55 | } 56 | .calendarium-day-name { 57 | display: none; 58 | } 59 | .showAllEvents, .showAllEvents:hover { 60 | text-decoration: none; 61 | display: none; 62 | i { 63 | margin-right: 2px; 64 | } 65 | } 66 | .alert { 67 | padding: 2px 4px; 68 | margin-bottom: 2px; 69 | margin-right: 17px; 70 | height: 12px; 71 | overflow: hidden; 72 | a, a:hover { 73 | font-size: 10px; 74 | font-weight: normal; 75 | text-decoration: none; 76 | white-space: nowrap; 77 | color: #333; 78 | border: none; 79 | text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.4); 80 | } 81 | .tooltip { 82 | text-shadow: none; 83 | font-weight: normal; 84 | white-space: nowrap; 85 | .tooltip-inner { 86 | max-width: 400px; 87 | } 88 | } 89 | } 90 | } 91 | } 92 | tr:last-child { 93 | td:nth-child(2) { 94 | border-left: 1px solid @calendar-border; 95 | } 96 | td { 97 | border-right: 1px solid @calendar-border; 98 | border-left: none; 99 | border-bottom: 1px solid @calendar-border; 100 | } 101 | td.calendarium-empty { 102 | border-top: 1px solid @calendar-border; 103 | border-bottom: none; 104 | border-left: none; 105 | border-right: none; 106 | } 107 | td.calendarium-week-link { 108 | border: none; 109 | } 110 | } 111 | } 112 | 113 | @media (max-width: 680px) { 114 | #calendar-month, #calendar-week { 115 | margin-top: 0px; 116 | th { 117 | display:none; 118 | } 119 | tr { 120 | td { 121 | display: block; 122 | width: auto; 123 | border-right: 1px solid #eee; 124 | } 125 | td.calendarium-empty { 126 | display: none; 127 | } 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /calendarium/static/calendarium/css/colorpicker.css: -------------------------------------------------------------------------------- 1 | .preview { 2 | display: block; 3 | float: left; 4 | margin-right: 10px; 5 | width: 20px; 6 | height: 20px; 7 | } 8 | 9 | .colorpicker { 10 | width: 356px; 11 | height: 176px; 12 | overflow: hidden; 13 | position: absolute; 14 | background: url(../img/colorpicker_background.png); 15 | font-family: Arial, Helvetica, sans-serif; 16 | display: none; 17 | } 18 | .colorpicker_color { 19 | width: 150px; 20 | height: 150px; 21 | left: 14px; 22 | top: 13px; 23 | position: absolute; 24 | background: #f00; 25 | overflow: hidden; 26 | cursor: crosshair; 27 | } 28 | .colorpicker_color div { 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | width: 150px; 33 | height: 150px; 34 | background: url(../img/colorpicker_overlay.png); 35 | } 36 | .colorpicker_color div div { 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | width: 11px; 41 | height: 11px; 42 | overflow: hidden; 43 | background: url(../img/colorpicker_select.gif); 44 | margin: -5px 0 0 -5px; 45 | } 46 | .colorpicker_hue { 47 | position: absolute; 48 | top: 13px; 49 | left: 171px; 50 | width: 35px; 51 | height: 150px; 52 | cursor: n-resize; 53 | } 54 | .colorpicker_hue div { 55 | position: absolute; 56 | width: 35px; 57 | height: 9px; 58 | overflow: hidden; 59 | background: url(../img/colorpicker_indic.gif) left top; 60 | margin: -4px 0 0 0; 61 | left: 0px; 62 | } 63 | .colorpicker_new_color { 64 | position: absolute; 65 | width: 60px; 66 | height: 30px; 67 | left: 213px; 68 | top: 13px; 69 | background: #f00; 70 | } 71 | .colorpicker_current_color { 72 | position: absolute; 73 | width: 60px; 74 | height: 30px; 75 | left: 283px; 76 | top: 13px; 77 | background: #f00; 78 | } 79 | .colorpicker input { 80 | background-color: transparent; 81 | border: 1px solid transparent; 82 | position: absolute; 83 | font-size: 10px; 84 | font-family: Arial, Helvetica, sans-serif; 85 | color: #898989; 86 | top: 4px; 87 | right: 11px; 88 | text-align: right; 89 | margin: 0; 90 | padding: 0; 91 | height: 11px; 92 | } 93 | .colorpicker_hex { 94 | position: absolute; 95 | width: 72px; 96 | height: 22px; 97 | background: url(../img/colorpicker_hex.png) top; 98 | left: 212px; 99 | top: 142px; 100 | } 101 | .colorpicker_hex input { 102 | right: 6px; 103 | } 104 | .colorpicker_field { 105 | height: 22px; 106 | width: 62px; 107 | background-position: top; 108 | position: absolute; 109 | } 110 | .colorpicker_field span { 111 | position: absolute; 112 | width: 12px; 113 | height: 22px; 114 | overflow: hidden; 115 | top: 0; 116 | right: 0; 117 | cursor: n-resize; 118 | } 119 | .colorpicker_rgb_r { 120 | background-image: url(../img/colorpicker_rgb_r.png); 121 | top: 52px; 122 | left: 212px; 123 | } 124 | .colorpicker_rgb_g { 125 | background-image: url(../img/colorpicker_rgb_g.png); 126 | top: 82px; 127 | left: 212px; 128 | } 129 | .colorpicker_rgb_b { 130 | background-image: url(../img/colorpicker_rgb_b.png); 131 | top: 112px; 132 | left: 212px; 133 | } 134 | .colorpicker_hsb_h { 135 | background-image: url(../img/colorpicker_hsb_h.png); 136 | top: 52px; 137 | left: 282px; 138 | } 139 | .colorpicker_hsb_s { 140 | background-image: url(../img/colorpicker_hsb_s.png); 141 | top: 82px; 142 | left: 282px; 143 | } 144 | .colorpicker_hsb_b { 145 | background-image: url(../img/colorpicker_hsb_b.png); 146 | top: 112px; 147 | left: 282px; 148 | } 149 | .colorpicker_submit { 150 | position: absolute; 151 | width: 22px; 152 | height: 22px; 153 | background: url(../img/colorpicker_submit.png) top; 154 | left: 322px; 155 | top: 142px; 156 | overflow: hidden; 157 | } 158 | .colorpicker_focus { 159 | background-position: center; 160 | } 161 | .colorpicker_hex.colorpicker_focus { 162 | background-position: bottom; 163 | } 164 | .colorpicker_submit.colorpicker_focus { 165 | background-position: bottom; 166 | } 167 | .colorpicker_slider { 168 | background-position: bottom; 169 | } 170 | -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/blank.gif -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_background.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_hex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_hex.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_hsb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_hsb_b.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_hsb_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_hsb_h.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_hsb_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_hsb_s.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_indic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_indic.gif -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_overlay.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_rgb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_rgb_b.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_rgb_g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_rgb_g.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_rgb_r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_rgb_r.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_select.gif -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/colorpicker_submit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/colorpicker_submit.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_background.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_hex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_hex.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_hsb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_hsb_b.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_hsb_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_hsb_h.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_hsb_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_hsb_s.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_indic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_indic.gif -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_rgb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_rgb_b.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_rgb_g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_rgb_g.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_rgb_r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_rgb_r.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/custom_submit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/custom_submit.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/select.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/select2.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/img/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/static/calendarium/img/slider.png -------------------------------------------------------------------------------- /calendarium/static/calendarium/js/colorpicker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Color picker 4 | * Author: Stefan Petre www.eyecon.ro 5 | * 6 | * Dual licensed under the MIT and GPL licenses 7 | * 8 | */ 9 | (function ($) { 10 | var ColorPicker = function () { 11 | var 12 | ids = {}, 13 | inAction, 14 | charMin = 65, 15 | visible, 16 | tpl = '