├── .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 = '
', 17 | defaults = { 18 | eventName: 'click', 19 | onShow: function () {}, 20 | onBeforeShow: function(){}, 21 | onHide: function () {}, 22 | onChange: function () {}, 23 | onSubmit: function () {}, 24 | color: 'ff0000', 25 | livePreview: true, 26 | flat: false 27 | }, 28 | fillRGBFields = function (hsb, cal) { 29 | var rgb = HSBToRGB(hsb); 30 | $(cal).data('colorpicker').fields 31 | .eq(1).val(rgb.r).end() 32 | .eq(2).val(rgb.g).end() 33 | .eq(3).val(rgb.b).end(); 34 | }, 35 | fillHSBFields = function (hsb, cal) { 36 | $(cal).data('colorpicker').fields 37 | .eq(4).val(hsb.h).end() 38 | .eq(5).val(hsb.s).end() 39 | .eq(6).val(hsb.b).end(); 40 | }, 41 | fillHexFields = function (hsb, cal) { 42 | $(cal).data('colorpicker').fields 43 | .eq(0).val(HSBToHex(hsb)).end(); 44 | }, 45 | setSelector = function (hsb, cal) { 46 | $(cal).data('colorpicker').selector.css('backgroundColor', '#' + HSBToHex({h: hsb.h, s: 100, b: 100})); 47 | $(cal).data('colorpicker').selectorIndic.css({ 48 | left: parseInt(150 * hsb.s/100, 10), 49 | top: parseInt(150 * (100-hsb.b)/100, 10) 50 | }); 51 | }, 52 | setHue = function (hsb, cal) { 53 | $(cal).data('colorpicker').hue.css('top', parseInt(150 - 150 * hsb.h/360, 10)); 54 | }, 55 | setCurrentColor = function (hsb, cal) { 56 | $(cal).data('colorpicker').currentColor.css('backgroundColor', '#' + HSBToHex(hsb)); 57 | }, 58 | setNewColor = function (hsb, cal) { 59 | $(cal).data('colorpicker').newColor.css('backgroundColor', '#' + HSBToHex(hsb)); 60 | }, 61 | keyDown = function (ev) { 62 | var pressedKey = ev.charCode || ev.keyCode || -1; 63 | if ((pressedKey > charMin && pressedKey <= 90) || pressedKey == 32) { 64 | return false; 65 | } 66 | var cal = $(this).parent().parent(); 67 | if (cal.data('colorpicker').livePreview === true) { 68 | change.apply(this); 69 | } 70 | }, 71 | change = function (ev) { 72 | var cal = $(this).parent().parent(), col; 73 | if (this.parentNode.className.indexOf('_hex') > 0) { 74 | cal.data('colorpicker').color = col = HexToHSB(fixHex(this.value)); 75 | } else if (this.parentNode.className.indexOf('_hsb') > 0) { 76 | cal.data('colorpicker').color = col = fixHSB({ 77 | h: parseInt(cal.data('colorpicker').fields.eq(4).val(), 10), 78 | s: parseInt(cal.data('colorpicker').fields.eq(5).val(), 10), 79 | b: parseInt(cal.data('colorpicker').fields.eq(6).val(), 10) 80 | }); 81 | } else { 82 | cal.data('colorpicker').color = col = RGBToHSB(fixRGB({ 83 | r: parseInt(cal.data('colorpicker').fields.eq(1).val(), 10), 84 | g: parseInt(cal.data('colorpicker').fields.eq(2).val(), 10), 85 | b: parseInt(cal.data('colorpicker').fields.eq(3).val(), 10) 86 | })); 87 | } 88 | if (ev) { 89 | fillRGBFields(col, cal.get(0)); 90 | fillHexFields(col, cal.get(0)); 91 | fillHSBFields(col, cal.get(0)); 92 | } 93 | setSelector(col, cal.get(0)); 94 | setHue(col, cal.get(0)); 95 | setNewColor(col, cal.get(0)); 96 | cal.data('colorpicker').onChange.apply(cal, [col, HSBToHex(col), HSBToRGB(col)]); 97 | }, 98 | blur = function (ev) { 99 | var cal = $(this).parent().parent(); 100 | cal.data('colorpicker').fields.parent().removeClass('colorpicker_focus'); 101 | }, 102 | focus = function () { 103 | charMin = this.parentNode.className.indexOf('_hex') > 0 ? 70 : 65; 104 | $(this).parent().parent().data('colorpicker').fields.parent().removeClass('colorpicker_focus'); 105 | $(this).parent().addClass('colorpicker_focus'); 106 | }, 107 | downIncrement = function (ev) { 108 | var field = $(this).parent().find('input').focus(); 109 | var current = { 110 | el: $(this).parent().addClass('colorpicker_slider'), 111 | max: this.parentNode.className.indexOf('_hsb_h') > 0 ? 360 : (this.parentNode.className.indexOf('_hsb') > 0 ? 100 : 255), 112 | y: ev.pageY, 113 | field: field, 114 | val: parseInt(field.val(), 10), 115 | preview: $(this).parent().parent().data('colorpicker').livePreview 116 | }; 117 | $(document).bind('mouseup', current, upIncrement); 118 | $(document).bind('mousemove', current, moveIncrement); 119 | }, 120 | moveIncrement = function (ev) { 121 | ev.data.field.val(Math.max(0, Math.min(ev.data.max, parseInt(ev.data.val + ev.pageY - ev.data.y, 10)))); 122 | if (ev.data.preview) { 123 | change.apply(ev.data.field.get(0), [true]); 124 | } 125 | return false; 126 | }, 127 | upIncrement = function (ev) { 128 | change.apply(ev.data.field.get(0), [true]); 129 | ev.data.el.removeClass('colorpicker_slider').find('input').focus(); 130 | $(document).unbind('mouseup', upIncrement); 131 | $(document).unbind('mousemove', moveIncrement); 132 | return false; 133 | }, 134 | downHue = function (ev) { 135 | var current = { 136 | cal: $(this).parent(), 137 | y: $(this).offset().top 138 | }; 139 | current.preview = current.cal.data('colorpicker').livePreview; 140 | $(document).bind('mouseup', current, upHue); 141 | $(document).bind('mousemove', current, moveHue); 142 | }, 143 | moveHue = function (ev) { 144 | change.apply( 145 | ev.data.cal.data('colorpicker') 146 | .fields 147 | .eq(4) 148 | .val(parseInt(360*(150 - Math.max(0,Math.min(150,(ev.pageY - ev.data.y))))/150, 10)) 149 | .get(0), 150 | [ev.data.preview] 151 | ); 152 | return false; 153 | }, 154 | upHue = function (ev) { 155 | fillRGBFields(ev.data.cal.data('colorpicker').color, ev.data.cal.get(0)); 156 | fillHexFields(ev.data.cal.data('colorpicker').color, ev.data.cal.get(0)); 157 | $(document).unbind('mouseup', upHue); 158 | $(document).unbind('mousemove', moveHue); 159 | return false; 160 | }, 161 | downSelector = function (ev) { 162 | var current = { 163 | cal: $(this).parent(), 164 | pos: $(this).offset() 165 | }; 166 | current.preview = current.cal.data('colorpicker').livePreview; 167 | $(document).bind('mouseup', current, upSelector); 168 | $(document).bind('mousemove', current, moveSelector); 169 | }, 170 | moveSelector = function (ev) { 171 | change.apply( 172 | ev.data.cal.data('colorpicker') 173 | .fields 174 | .eq(6) 175 | .val(parseInt(100*(150 - Math.max(0,Math.min(150,(ev.pageY - ev.data.pos.top))))/150, 10)) 176 | .end() 177 | .eq(5) 178 | .val(parseInt(100*(Math.max(0,Math.min(150,(ev.pageX - ev.data.pos.left))))/150, 10)) 179 | .get(0), 180 | [ev.data.preview] 181 | ); 182 | return false; 183 | }, 184 | upSelector = function (ev) { 185 | fillRGBFields(ev.data.cal.data('colorpicker').color, ev.data.cal.get(0)); 186 | fillHexFields(ev.data.cal.data('colorpicker').color, ev.data.cal.get(0)); 187 | $(document).unbind('mouseup', upSelector); 188 | $(document).unbind('mousemove', moveSelector); 189 | return false; 190 | }, 191 | enterSubmit = function (ev) { 192 | $(this).addClass('colorpicker_focus'); 193 | }, 194 | leaveSubmit = function (ev) { 195 | $(this).removeClass('colorpicker_focus'); 196 | }, 197 | clickSubmit = function (ev) { 198 | var cal = $(this).parent(); 199 | var col = cal.data('colorpicker').color; 200 | cal.data('colorpicker').origColor = col; 201 | setCurrentColor(col, cal.get(0)); 202 | cal.data('colorpicker').onSubmit(col, HSBToHex(col), HSBToRGB(col), cal.data('colorpicker').el); 203 | }, 204 | show = function (ev) { 205 | var cal = $('#' + $(this).data('colorpickerId')); 206 | cal.data('colorpicker').onBeforeShow.apply(this, [cal.get(0)]); 207 | var pos = $(this).offset(); 208 | var viewPort = getViewport(); 209 | var top = pos.top + this.offsetHeight; 210 | var left = pos.left; 211 | if (top + 176 > viewPort.t + viewPort.h) { 212 | top -= this.offsetHeight + 176; 213 | } 214 | if (left + 356 > viewPort.l + viewPort.w) { 215 | left -= 356; 216 | } 217 | cal.css({left: left + 'px', top: top + 'px'}); 218 | if (cal.data('colorpicker').onShow.apply(this, [cal.get(0)]) != false) { 219 | cal.show(); 220 | } 221 | $(document).bind('mousedown', {cal: cal}, hide); 222 | return false; 223 | }, 224 | hide = function (ev) { 225 | if (!isChildOf(ev.data.cal.get(0), ev.target, ev.data.cal.get(0))) { 226 | if (ev.data.cal.data('colorpicker').onHide.apply(this, [ev.data.cal.get(0)]) != false) { 227 | ev.data.cal.hide(); 228 | } 229 | $(document).unbind('mousedown', hide); 230 | } 231 | }, 232 | isChildOf = function(parentEl, el, container) { 233 | if (parentEl == el) { 234 | return true; 235 | } 236 | if (parentEl.contains) { 237 | return parentEl.contains(el); 238 | } 239 | if ( parentEl.compareDocumentPosition ) { 240 | return !!(parentEl.compareDocumentPosition(el) & 16); 241 | } 242 | var prEl = el.parentNode; 243 | while(prEl && prEl != container) { 244 | if (prEl == parentEl) 245 | return true; 246 | prEl = prEl.parentNode; 247 | } 248 | return false; 249 | }, 250 | getViewport = function () { 251 | var m = document.compatMode == 'CSS1Compat'; 252 | return { 253 | l : window.pageXOffset || (m ? document.documentElement.scrollLeft : document.body.scrollLeft), 254 | t : window.pageYOffset || (m ? document.documentElement.scrollTop : document.body.scrollTop), 255 | w : window.innerWidth || (m ? document.documentElement.clientWidth : document.body.clientWidth), 256 | h : window.innerHeight || (m ? document.documentElement.clientHeight : document.body.clientHeight) 257 | }; 258 | }, 259 | fixHSB = function (hsb) { 260 | return { 261 | h: Math.min(360, Math.max(0, hsb.h)), 262 | s: Math.min(100, Math.max(0, hsb.s)), 263 | b: Math.min(100, Math.max(0, hsb.b)) 264 | }; 265 | }, 266 | fixRGB = function (rgb) { 267 | return { 268 | r: Math.min(255, Math.max(0, rgb.r)), 269 | g: Math.min(255, Math.max(0, rgb.g)), 270 | b: Math.min(255, Math.max(0, rgb.b)) 271 | }; 272 | }, 273 | fixHex = function (hex) { 274 | var len = 6 - hex.length; 275 | if (len > 0) { 276 | var o = []; 277 | for (var i=0; i -1) ? hex.substring(1) : hex), 16); 287 | return {r: hex >> 16, g: (hex & 0x00FF00) >> 8, b: (hex & 0x0000FF)}; 288 | }, 289 | HexToHSB = function (hex) { 290 | return RGBToHSB(HexToRGB(hex)); 291 | }, 292 | RGBToHSB = function (rgb) { 293 | var hsb = { 294 | h: 0, 295 | s: 0, 296 | b: 0 297 | }; 298 | var min = Math.min(rgb.r, rgb.g, rgb.b); 299 | var max = Math.max(rgb.r, rgb.g, rgb.b); 300 | var delta = max - min; 301 | hsb.b = max; 302 | if (max != 0) { 303 | 304 | } 305 | hsb.s = max != 0 ? 255 * delta / max : 0; 306 | if (hsb.s != 0) { 307 | if (rgb.r == max) { 308 | hsb.h = (rgb.g - rgb.b) / delta; 309 | } else if (rgb.g == max) { 310 | hsb.h = 2 + (rgb.b - rgb.r) / delta; 311 | } else { 312 | hsb.h = 4 + (rgb.r - rgb.g) / delta; 313 | } 314 | } else { 315 | hsb.h = -1; 316 | } 317 | hsb.h *= 60; 318 | if (hsb.h < 0) { 319 | hsb.h += 360; 320 | } 321 | hsb.s *= 100/255; 322 | hsb.b *= 100/255; 323 | return hsb; 324 | }, 325 | HSBToRGB = function (hsb) { 326 | var rgb = {}; 327 | var h = Math.round(hsb.h); 328 | var s = Math.round(hsb.s*255/100); 329 | var v = Math.round(hsb.b*255/100); 330 | if(s == 0) { 331 | rgb.r = rgb.g = rgb.b = v; 332 | } else { 333 | var t1 = v; 334 | var t2 = (255-s)*v/255; 335 | var t3 = (t1-t2)*(h%60)/60; 336 | if(h==360) h = 0; 337 | if(h<60) {rgb.r=t1; rgb.b=t2; rgb.g=t2+t3} 338 | else if(h<120) {rgb.g=t1; rgb.b=t2; rgb.r=t1-t3} 339 | else if(h<180) {rgb.g=t1; rgb.r=t2; rgb.b=t2+t3} 340 | else if(h<240) {rgb.b=t1; rgb.r=t2; rgb.g=t1-t3} 341 | else if(h<300) {rgb.b=t1; rgb.g=t2; rgb.r=t2+t3} 342 | else if(h<360) {rgb.r=t1; rgb.g=t2; rgb.b=t1-t3} 343 | else {rgb.r=0; rgb.g=0; rgb.b=0} 344 | } 345 | return {r:Math.round(rgb.r), g:Math.round(rgb.g), b:Math.round(rgb.b)}; 346 | }, 347 | RGBToHex = function (rgb) { 348 | var hex = [ 349 | rgb.r.toString(16), 350 | rgb.g.toString(16), 351 | rgb.b.toString(16) 352 | ]; 353 | $.each(hex, function (nr, val) { 354 | if (val.length == 1) { 355 | hex[nr] = '0' + val; 356 | } 357 | }); 358 | return hex.join(''); 359 | }, 360 | HSBToHex = function (hsb) { 361 | return RGBToHex(HSBToRGB(hsb)); 362 | }, 363 | restoreOriginal = function () { 364 | var cal = $(this).parent(); 365 | var col = cal.data('colorpicker').origColor; 366 | cal.data('colorpicker').color = col; 367 | fillRGBFields(col, cal.get(0)); 368 | fillHexFields(col, cal.get(0)); 369 | fillHSBFields(col, cal.get(0)); 370 | setSelector(col, cal.get(0)); 371 | setHue(col, cal.get(0)); 372 | setNewColor(col, cal.get(0)); 373 | }; 374 | return { 375 | init: function (opt) { 376 | opt = $.extend({}, defaults, opt||{}); 377 | if (typeof opt.color == 'string') { 378 | opt.color = HexToHSB(opt.color); 379 | } else if (opt.color.r != undefined && opt.color.g != undefined && opt.color.b != undefined) { 380 | opt.color = RGBToHSB(opt.color); 381 | } else if (opt.color.h != undefined && opt.color.s != undefined && opt.color.b != undefined) { 382 | opt.color = fixHSB(opt.color); 383 | } else { 384 | return this; 385 | } 386 | return this.each(function () { 387 | if (!$(this).data('colorpickerId')) { 388 | var options = $.extend({}, opt); 389 | options.origColor = opt.color; 390 | var id = 'collorpicker_' + parseInt(Math.random() * 1000); 391 | $(this).data('colorpickerId', id); 392 | var cal = $(tpl).attr('id', id); 393 | if (options.flat) { 394 | cal.appendTo(this).show(); 395 | } else { 396 | cal.appendTo(document.body); 397 | } 398 | options.fields = cal 399 | .find('input') 400 | .bind('keyup', keyDown) 401 | .bind('change', change) 402 | .bind('blur', blur) 403 | .bind('focus', focus); 404 | cal 405 | .find('span').bind('mousedown', downIncrement).end() 406 | .find('>div.colorpicker_current_color').bind('click', restoreOriginal); 407 | options.selector = cal.find('div.colorpicker_color').bind('mousedown', downSelector); 408 | options.selectorIndic = options.selector.find('div div'); 409 | options.el = this; 410 | options.hue = cal.find('div.colorpicker_hue div'); 411 | cal.find('div.colorpicker_hue').bind('mousedown', downHue); 412 | options.newColor = cal.find('div.colorpicker_new_color'); 413 | options.currentColor = cal.find('div.colorpicker_current_color'); 414 | cal.data('colorpicker', options); 415 | cal.find('div.colorpicker_submit') 416 | .bind('mouseenter', enterSubmit) 417 | .bind('mouseleave', leaveSubmit) 418 | .bind('click', clickSubmit); 419 | fillRGBFields(options.color, cal.get(0)); 420 | fillHSBFields(options.color, cal.get(0)); 421 | fillHexFields(options.color, cal.get(0)); 422 | setHue(options.color, cal.get(0)); 423 | setSelector(options.color, cal.get(0)); 424 | setCurrentColor(options.color, cal.get(0)); 425 | setNewColor(options.color, cal.get(0)); 426 | if (options.flat) { 427 | cal.css({ 428 | position: 'relative', 429 | display: 'block' 430 | }); 431 | } else { 432 | $(this).bind(options.eventName, show); 433 | } 434 | } 435 | }); 436 | }, 437 | showPicker: function() { 438 | return this.each( function () { 439 | if ($(this).data('colorpickerId')) { 440 | show.apply(this); 441 | } 442 | }); 443 | }, 444 | hidePicker: function() { 445 | return this.each( function () { 446 | if ($(this).data('colorpickerId')) { 447 | $('#' + $(this).data('colorpickerId')).hide(); 448 | } 449 | }); 450 | }, 451 | setColor: function(col) { 452 | if (typeof col == 'string') { 453 | col = HexToHSB(col); 454 | } else if (col.r != undefined && col.g != undefined && col.b != undefined) { 455 | col = RGBToHSB(col); 456 | } else if (col.h != undefined && col.s != undefined && col.b != undefined) { 457 | col = fixHSB(col); 458 | } else { 459 | return this; 460 | } 461 | return this.each(function(){ 462 | if ($(this).data('colorpickerId')) { 463 | var cal = $('#' + $(this).data('colorpickerId')); 464 | cal.data('colorpicker').color = col; 465 | cal.data('colorpicker').origColor = col; 466 | fillRGBFields(col, cal.get(0)); 467 | fillHSBFields(col, cal.get(0)); 468 | fillHexFields(col, cal.get(0)); 469 | setHue(col, cal.get(0)); 470 | setSelector(col, cal.get(0)); 471 | setCurrentColor(col, cal.get(0)); 472 | setNewColor(col, cal.get(0)); 473 | } 474 | }); 475 | } 476 | }; 477 | }(); 478 | $.fn.extend({ 479 | ColorPicker: ColorPicker.init, 480 | ColorPickerHide: ColorPicker.hidePicker, 481 | ColorPickerShow: ColorPicker.showPicker, 482 | ColorPickerSetColor: ColorPicker.setColor 483 | }); 484 | })(jQuery) -------------------------------------------------------------------------------- /calendarium/static/calendarium/js/colorpicker_list.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('#result_list input[type="text"]').each(function() { 3 | $('
').insertBefore($(this)); 4 | }); 5 | }); -------------------------------------------------------------------------------- /calendarium/static/calendarium/js/eye.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Zoomimage 4 | * Author: Stefan Petre www.eyecon.ro 5 | * 6 | */ 7 | (function($){ 8 | var EYE = window.EYE = function() { 9 | var _registered = { 10 | init: [] 11 | }; 12 | return { 13 | init: function() { 14 | $.each(_registered.init, function(nr, fn){ 15 | fn.call(); 16 | }); 17 | }, 18 | extend: function(prop) { 19 | for (var i in prop) { 20 | if (prop[i] != undefined) { 21 | this[i] = prop[i]; 22 | } 23 | } 24 | }, 25 | register: function(fn, type) { 26 | if (!_registered[type]) { 27 | _registered[type] = []; 28 | } 29 | _registered[type].push(fn); 30 | } 31 | }; 32 | }(); 33 | $(EYE.init); 34 | })(jQuery); 35 | -------------------------------------------------------------------------------- /calendarium/static/calendarium/js/layout.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | var initLayout = function() { 3 | var hash = window.location.hash.replace('#', ''); 4 | var currentTab = $('ul.navigationTabs a') 5 | .bind('click', showTab) 6 | .filter('a[rel=' + hash + ']'); 7 | if (currentTab.size() == 0) { 8 | currentTab = $('ul.navigationTabs a:first'); 9 | } 10 | showTab.apply(currentTab.get(0)); 11 | $('#colorpickerHolder').ColorPicker({flat: true}); 12 | $('#colorpickerHolder2').ColorPicker({ 13 | flat: true, 14 | color: '#00ff00', 15 | onSubmit: function(hsb, hex, rgb) { 16 | $('#colorSelector2 div').css('backgroundColor', '#' + hex); 17 | } 18 | }); 19 | $('#colorpickerHolder2>div').css('position', 'absolute'); 20 | var widt = false; 21 | $('#colorSelector2').bind('click', function() { 22 | $('#colorpickerHolder2').stop().animate({height: widt ? 0 : 173}, 500); 23 | widt = !widt; 24 | }); 25 | $('#colorpickerField1, #colorpickerField2, #colorpickerField3').ColorPicker({ 26 | onSubmit: function(hsb, hex, rgb, el) { 27 | $(el).val(hex); 28 | $(el).ColorPickerHide(); 29 | }, 30 | onBeforeShow: function () { 31 | $(this).ColorPickerSetColor(this.value); 32 | } 33 | }) 34 | .bind('keyup', function(){ 35 | $(this).ColorPickerSetColor(this.value); 36 | }); 37 | $('#colorSelector').ColorPicker({ 38 | color: '#0000ff', 39 | onShow: function (colpkr) { 40 | $(colpkr).fadeIn(500); 41 | return false; 42 | }, 43 | onHide: function (colpkr) { 44 | $(colpkr).fadeOut(500); 45 | return false; 46 | }, 47 | onChange: function (hsb, hex, rgb) { 48 | $('#colorSelector div').css('backgroundColor', '#' + hex); 49 | } 50 | }); 51 | }; 52 | 53 | var showTab = function(e) { 54 | var tabIndex = $('ul.navigationTabs a') 55 | .removeClass('active') 56 | .index(this); 57 | $(this) 58 | .addClass('active') 59 | .blur(); 60 | $('div.tab') 61 | .hide() 62 | .eq(tabIndex) 63 | .show(); 64 | }; 65 | 66 | EYE.register(initLayout, 'init'); 67 | })(jQuery) -------------------------------------------------------------------------------- /calendarium/static/calendarium/js/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Utilities 4 | * Author: Stefan Petre www.eyecon.ro 5 | * 6 | */ 7 | (function($) { 8 | EYE.extend({ 9 | getPosition : function(e, forceIt) 10 | { 11 | var x = 0; 12 | var y = 0; 13 | var es = e.style; 14 | var restoreStyles = false; 15 | if (forceIt && jQuery.curCSS(e,'display') == 'none') { 16 | var oldVisibility = es.visibility; 17 | var oldPosition = es.position; 18 | restoreStyles = true; 19 | es.visibility = 'hidden'; 20 | es.display = 'block'; 21 | es.position = 'absolute'; 22 | } 23 | var el = e; 24 | if (el.getBoundingClientRect) { // IE 25 | var box = el.getBoundingClientRect(); 26 | x = box.left + Math.max(document.documentElement.scrollLeft, document.body.scrollLeft) - 2; 27 | y = box.top + Math.max(document.documentElement.scrollTop, document.body.scrollTop) - 2; 28 | } else { 29 | x = el.offsetLeft; 30 | y = el.offsetTop; 31 | el = el.offsetParent; 32 | if (e != el) { 33 | while (el) { 34 | x += el.offsetLeft; 35 | y += el.offsetTop; 36 | el = el.offsetParent; 37 | } 38 | } 39 | if (jQuery.browser.safari && jQuery.curCSS(e, 'position') == 'absolute' ) { 40 | x -= document.body.offsetLeft; 41 | y -= document.body.offsetTop; 42 | } 43 | el = e.parentNode; 44 | while (el && el.tagName.toUpperCase() != 'BODY' && el.tagName.toUpperCase() != 'HTML') 45 | { 46 | if (jQuery.curCSS(el, 'display') != 'inline') { 47 | x -= el.scrollLeft; 48 | y -= el.scrollTop; 49 | } 50 | el = el.parentNode; 51 | } 52 | } 53 | if (restoreStyles == true) { 54 | es.display = 'none'; 55 | es.position = oldPosition; 56 | es.visibility = oldVisibility; 57 | } 58 | return {x:x, y:y}; 59 | }, 60 | getSize : function(e) 61 | { 62 | var w = parseInt(jQuery.curCSS(e,'width'), 10); 63 | var h = parseInt(jQuery.curCSS(e,'height'), 10); 64 | var wb = 0; 65 | var hb = 0; 66 | if (jQuery.curCSS(e, 'display') != 'none') { 67 | wb = e.offsetWidth; 68 | hb = e.offsetHeight; 69 | } else { 70 | var es = e.style; 71 | var oldVisibility = es.visibility; 72 | var oldPosition = es.position; 73 | es.visibility = 'hidden'; 74 | es.display = 'block'; 75 | es.position = 'absolute'; 76 | wb = e.offsetWidth; 77 | hb = e.offsetHeight; 78 | es.display = 'none'; 79 | es.position = oldPosition; 80 | es.visibility = oldVisibility; 81 | } 82 | return {w:w, h:h, wb:wb, hb:hb}; 83 | }, 84 | getClient : function(e) 85 | { 86 | var h, w; 87 | if (e) { 88 | w = e.clientWidth; 89 | h = e.clientHeight; 90 | } else { 91 | var de = document.documentElement; 92 | w = window.innerWidth || self.innerWidth || (de&&de.clientWidth) || document.body.clientWidth; 93 | h = window.innerHeight || self.innerHeight || (de&&de.clientHeight) || document.body.clientHeight; 94 | } 95 | return {w:w,h:h}; 96 | }, 97 | getScroll : function (e) 98 | { 99 | var t=0, l=0, w=0, h=0, iw=0, ih=0; 100 | if (e && e.nodeName.toLowerCase() != 'body') { 101 | t = e.scrollTop; 102 | l = e.scrollLeft; 103 | w = e.scrollWidth; 104 | h = e.scrollHeight; 105 | } else { 106 | if (document.documentElement) { 107 | t = document.documentElement.scrollTop; 108 | l = document.documentElement.scrollLeft; 109 | w = document.documentElement.scrollWidth; 110 | h = document.documentElement.scrollHeight; 111 | } else if (document.body) { 112 | t = document.body.scrollTop; 113 | l = document.body.scrollLeft; 114 | w = document.body.scrollWidth; 115 | h = document.body.scrollHeight; 116 | } 117 | if (typeof pageYOffset != 'undefined') { 118 | t = pageYOffset; 119 | l = pageXOffset; 120 | } 121 | iw = self.innerWidth||document.documentElement.clientWidth||document.body.clientWidth||0; 122 | ih = self.innerHeight||document.documentElement.clientHeight||document.body.clientHeight||0; 123 | } 124 | return { t: t, l: l, w: w, h: h, iw: iw, ih: ih }; 125 | }, 126 | getMargins : function(e, toInteger) 127 | { 128 | var t = jQuery.curCSS(e,'marginTop') || ''; 129 | var r = jQuery.curCSS(e,'marginRight') || ''; 130 | var b = jQuery.curCSS(e,'marginBottom') || ''; 131 | var l = jQuery.curCSS(e,'marginLeft') || ''; 132 | if (toInteger) 133 | return { 134 | t: parseInt(t, 10)||0, 135 | r: parseInt(r, 10)||0, 136 | b: parseInt(b, 10)||0, 137 | l: parseInt(l, 10) 138 | }; 139 | else 140 | return {t: t, r: r, b: b, l: l}; 141 | }, 142 | getPadding : function(e, toInteger) 143 | { 144 | var t = jQuery.curCSS(e,'paddingTop') || ''; 145 | var r = jQuery.curCSS(e,'paddingRight') || ''; 146 | var b = jQuery.curCSS(e,'paddingBottom') || ''; 147 | var l = jQuery.curCSS(e,'paddingLeft') || ''; 148 | if (toInteger) 149 | return { 150 | t: parseInt(t, 10)||0, 151 | r: parseInt(r, 10)||0, 152 | b: parseInt(b, 10)||0, 153 | l: parseInt(l, 10) 154 | }; 155 | else 156 | return {t: t, r: r, b: b, l: l}; 157 | }, 158 | getBorder : function(e, toInteger) 159 | { 160 | var t = jQuery.curCSS(e,'borderTopWidth') || ''; 161 | var r = jQuery.curCSS(e,'borderRightWidth') || ''; 162 | var b = jQuery.curCSS(e,'borderBottomWidth') || ''; 163 | var l = jQuery.curCSS(e,'borderLeftWidth') || ''; 164 | if (toInteger) 165 | return { 166 | t: parseInt(t, 10)||0, 167 | r: parseInt(r, 10)||0, 168 | b: parseInt(b, 10)||0, 169 | l: parseInt(l, 10)||0 170 | }; 171 | else 172 | return {t: t, r: r, b: b, l: l}; 173 | }, 174 | traverseDOM : function(nodeEl, func) 175 | { 176 | func(nodeEl); 177 | nodeEl = nodeEl.firstChild; 178 | while(nodeEl){ 179 | EYE.traverseDOM(nodeEl, func); 180 | nodeEl = nodeEl.nextSibling; 181 | } 182 | }, 183 | getInnerWidth : function(el, scroll) { 184 | var offsetW = el.offsetWidth; 185 | return scroll ? Math.max(el.scrollWidth,offsetW) - offsetW + el.clientWidth:el.clientWidth; 186 | }, 187 | getInnerHeight : function(el, scroll) { 188 | var offsetH = el.offsetHeight; 189 | return scroll ? Math.max(el.scrollHeight,offsetH) - offsetH + el.clientHeight:el.clientHeight; 190 | }, 191 | getExtraWidth : function(el) { 192 | if($.boxModel) 193 | return (parseInt($.curCSS(el, 'paddingLeft'))||0) 194 | + (parseInt($.curCSS(el, 'paddingRight'))||0) 195 | + (parseInt($.curCSS(el, 'borderLeftWidth'))||0) 196 | + (parseInt($.curCSS(el, 'borderRightWidth'))||0); 197 | return 0; 198 | }, 199 | getExtraHeight : function(el) { 200 | if($.boxModel) 201 | return (parseInt($.curCSS(el, 'paddingTop'))||0) 202 | + (parseInt($.curCSS(el, 'paddingBottom'))||0) 203 | + (parseInt($.curCSS(el, 'borderTopWidth'))||0) 204 | + (parseInt($.curCSS(el, 'borderBottomWidth'))||0); 205 | return 0; 206 | }, 207 | isChildOf: function(parentEl, el, container) { 208 | if (parentEl == el) { 209 | return true; 210 | } 211 | if (!el || !el.nodeType || el.nodeType != 1) { 212 | return false; 213 | } 214 | if (parentEl.contains && !$.browser.safari) { 215 | return parentEl.contains(el); 216 | } 217 | if ( parentEl.compareDocumentPosition ) { 218 | return !!(parentEl.compareDocumentPosition(el) & 16); 219 | } 220 | var prEl = el.parentNode; 221 | while(prEl && prEl != container) { 222 | if (prEl == parentEl) 223 | return true; 224 | prEl = prEl.parentNode; 225 | } 226 | return false; 227 | }, 228 | centerEl : function(el, axis) 229 | { 230 | var clientScroll = EYE.getScroll(); 231 | var size = EYE.getSize(el); 232 | if (!axis || axis == 'vertically') 233 | $(el).css( 234 | { 235 | top: clientScroll.t + ((Math.min(clientScroll.h,clientScroll.ih) - size.hb)/2) + 'px' 236 | } 237 | ); 238 | if (!axis || axis == 'horizontally') 239 | $(el).css( 240 | { 241 | left: clientScroll.l + ((Math.min(clientScroll.w,clientScroll.iw) - size.wb)/2) + 'px' 242 | } 243 | ); 244 | } 245 | }); 246 | if (!$.easing.easeout) { 247 | $.easing.easeout = function(p, n, firstNum, delta, duration) { 248 | return -delta * ((n=n/duration-1)*n*n*n - 1) + firstNum; 249 | }; 250 | } 251 | 252 | })(jQuery); -------------------------------------------------------------------------------- /calendarium/templates/404.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/templates/404.html -------------------------------------------------------------------------------- /calendarium/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% block main %}{% endblock %} 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/calendar_day.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |

{% trans "Occurrences" %}

6 | {% include "calendarium/partials/category_list.html" %} 7 |
8 | {% csrf_token %} 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | {% for occurrence in occurrences %} 19 | 20 | {% endfor %} 21 | 22 |
{{ date|date:'D m/d' }}

{{ occurrence }}

23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/calendar_month.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n calendarium_tags %} 3 | 4 | {% block main %} 5 |

{{ date|date:"F Y" }}

6 | {% include "calendarium/partials/category_list.html" %} 7 |
8 | {% csrf_token %} 9 | 10 | 11 | 12 | {% if request.user.is_staff %} 13 | {% trans "Create new event" %} 14 | {% endif %} 15 |
16 | 17 | 18 | 19 | {% for weekday in weekdays %} 20 | 21 | {% endfor %} 22 | 23 | {% for week in month %} 24 | {% if week %} 25 | 26 | 27 | {% for day, occurrences, current in week %} 28 | 40 | {% endfor %} 41 | 42 | {% endif %} 43 | {% endfor %} 44 |
{{ weekday }}
29 |
30 | {% if day != 0 %} 31 | {{ day }} 32 | {% for occurrence in occurrences %} 33 |

34 | {{ occurrence|truncatechars:22 }} 35 |

36 | {% endfor %} 37 | {% endif %} 38 |
39 |
45 | {% render_upcoming_events 5 current_category %} 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/calendar_week.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |

{% trans "Occurrences" %}

6 | {% include "calendarium/partials/category_list.html" %} 7 |
8 | {% csrf_token %} 9 | 10 | 11 | 12 |
13 | 14 | 15 | {% for date, occurrences, current in week %} 16 | 17 | {% endfor %} 18 | 19 | 20 | {% for date, occurrences, current in week %} 21 | 32 | {% endfor %} 33 | 34 |
{{ date|date:'D m/d' }}
22 |
23 | {{ date|date:'D m/d' }} 24 | {% for occurrence in occurrences %} 25 |

26 | {{ occurrence|truncatechars:22 }} 27 |

28 | {% endfor %} 29 | {% trans "View calendar day" %} 30 |
31 |
35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/event_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |

{% trans "Delete" %} {{ object }}

6 |
7 | {% csrf_token %} 8 | {% trans "Do you really want to delete this event?" %} 9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/event_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |

{{ object }}

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
{% trans "Start" %}{{ object.start }}
{% trans "End" %}{{ object.end }}
{% trans "Category" %}{{ object.category }}
{% trans "Description" %}{{ object.description }}
24 | {% trans "back" %} 25 | {% if request.user.is_staff %} 26 | {% trans "Edit" %} 27 | {% trans "Delete" %} 28 | {% endif %} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/event_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |

6 | {% if form.instance.id %} 7 | {% trans "Update" %} {{ form.instance }} 8 | {% else %} 9 | {% trans "Create event" %} 10 | {% endif %} 11 |

12 |
13 | {% csrf_token %} 14 | {{ form.as_p }} 15 | 16 |
17 | {% if form.instance.id %} 18 | {% trans "Delete" %} 19 | {% endif %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/occurrence_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |

{% trans "Delete" %} {{ object }}

6 |
7 | {% csrf_token %} 8 |

9 | 10 | 15 |

16 | {% trans "Back" %} 17 | 18 |
19 | {% endblock %} 20 | 21 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/occurrence_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |

{{ object }}

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
{% trans "Start" %}{{ object.start }}
{% trans "End" %}{{ object.end }}
{% trans "Category" %}{{ object.category }}
{% trans "Description" %}{{ object.description }}
24 | {% trans "back" %} 25 | {% if request.user.is_staff %} 26 | {% trans "Edit" %} 27 | {% trans "Delete" %} 28 | {% endif %} 29 | {% endblock %} 30 | 31 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/occurrence_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block main %} 5 |

6 | {% if form.instance.id %} 7 | {% trans "Update" %} {{ form.instance }} 8 | {% else %} 9 | {% trans "Create event" %} 10 | {% endif %} 11 |

12 |
13 | {% csrf_token %} 14 | {{ form.as_p }} 15 | 16 |
17 | {% trans "Delete" %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/partials/calendar_day.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/partials/calendar_month.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/partials/calendar_week.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/partials/category_list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/partials/upcoming_events.html: -------------------------------------------------------------------------------- 1 | {% for occurrence in occurrences %} 2 | {{ occurrence.event }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /calendarium/templates/calendarium/upcoming_events.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /calendarium/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/templatetags/__init__.py -------------------------------------------------------------------------------- /calendarium/templatetags/calendarium_tags.py: -------------------------------------------------------------------------------- 1 | """Templatetags for the ``calendarium`` project.""" 2 | from django.urls import reverse 3 | from django import template 4 | from django.utils.timezone import datetime, now, timedelta, utc 5 | 6 | from ..models import Event, EventCategory 7 | 8 | register = template.Library() 9 | 10 | 11 | @register.filter 12 | def get_week_URL(date, day=0): 13 | """ 14 | Returns the week view URL for a given date. 15 | 16 | :param date: A date instance. 17 | :param day: Day number in a month. 18 | 19 | """ 20 | if day < 1: 21 | day = 1 22 | date = datetime(year=date.year, month=date.month, day=day, tzinfo=utc) 23 | return reverse('calendar_week', kwargs={'year': date.isocalendar()[0], 24 | 'week': date.isocalendar()[1]}) 25 | 26 | 27 | def _get_upcoming_events(amount=5, category=None): 28 | if not isinstance(category, EventCategory): 29 | category = None 30 | return Event.objects.get_occurrences( 31 | now(), now() + timedelta(days=356), category)[:amount] 32 | 33 | 34 | @register.inclusion_tag('calendarium/upcoming_events.html') 35 | def render_upcoming_events(event_amount=5, category=None): 36 | """Template tag to render a list of upcoming events.""" 37 | return { 38 | 'occurrences': _get_upcoming_events( 39 | amount=event_amount, category=category), 40 | } 41 | 42 | 43 | @register.simple_tag 44 | def get_upcoming_events(amount=5, category=None): 45 | """Returns a list of upcoming events.""" 46 | return _get_upcoming_events(amount=amount, category=category) 47 | -------------------------------------------------------------------------------- /calendarium/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/tests/__init__.py -------------------------------------------------------------------------------- /calendarium/tests/forms_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the forms of the ``calendarium`` app.""" 2 | import json 3 | 4 | from django.forms.models import model_to_dict 5 | from django.test import TestCase 6 | from django.utils.timezone import timedelta 7 | 8 | from mixer.backend.django import mixer 9 | 10 | from ..constants import FREQUENCIES, OCCURRENCE_DECISIONS 11 | from ..forms import OccurrenceForm 12 | from ..models import Event, Occurrence 13 | from ..utils import now 14 | 15 | 16 | class OccurrenceFormTestCase(TestCase): 17 | """Test for the ``OccurrenceForm`` form class.""" 18 | longMessage = True 19 | 20 | def setUp(self): 21 | # single, not recurring event 22 | self.event = mixer.blend('calendarium.Event', rule=None, 23 | end_recurring_period=None) 24 | self.event_occurrence = next(self.event.get_occurrences( 25 | self.event.start)) 26 | 27 | # recurring event weekly on mondays over 6 weeks 28 | self.rule = mixer.blend( 29 | 'calendarium.Rule', 30 | name='weekly', frequency=FREQUENCIES['WEEKLY'], 31 | params=json.dumps({'byweekday': 0})) 32 | self.rec_event = mixer.blend( 33 | 'calendarium.Event', 34 | rule=self.rule, start=now(), 35 | end_recurring_period=now() + timedelta(days=41), 36 | ) 37 | self.rec_occurrence_list = [ 38 | occ for occ in self.rec_event.get_occurrences( 39 | self.rec_event.start, self.rec_event.end_recurring_period)] 40 | self.rec_occurrence = self.rec_occurrence_list[1] 41 | 42 | def test_form(self): 43 | """Test if ``OccurrenceForm`` is valid and saves correctly.""" 44 | # Test for not recurring event 45 | data = model_to_dict(self.event_occurrence) 46 | initial = data.copy() 47 | data.update({ 48 | 'decision': OCCURRENCE_DECISIONS['all'], 49 | 'title': 'changed'}) 50 | form = OccurrenceForm(data=data, initial=initial) 51 | self.assertTrue(form.is_valid(), msg=( 52 | 'The OccurrenceForm should be valid')) 53 | form.save() 54 | event = Event.objects.get(pk=self.event.pk) 55 | self.assertEqual(event.title, 'changed', msg=( 56 | 'When save is called, the event\'s title should be "changed".')) 57 | 58 | # Test for recurring event 59 | 60 | # Case 1: Altering occurrence 3 to be on a tuesday. 61 | data = model_to_dict(self.rec_occurrence) 62 | initial = data.copy() 63 | data.update({ 64 | 'decision': OCCURRENCE_DECISIONS['this one'], 65 | 'title': 'different'}) 66 | form = OccurrenceForm(data=data, initial=initial) 67 | self.assertTrue(form.is_valid(), msg=( 68 | 'The OccurrenceForm should be valid')) 69 | form.save() 70 | self.assertEqual(Occurrence.objects.all().count(), 1, msg=( 71 | 'After one occurrence has changed, there should be one persistent' 72 | ' occurrence.')) 73 | occ = Occurrence.objects.get() 74 | self.assertEqual(occ.title, 'different', msg=( 75 | 'When save is called, the occurrence\'s title should be' 76 | ' "different".')) 77 | 78 | # Case 2: Altering the description of "all" on the first occurrence 79 | # should also change 3rd one 80 | occ_to_use = self.rec_occurrence_list[0] 81 | data = model_to_dict(occ_to_use) 82 | initial = data.copy() 83 | new_start = occ_to_use.start + timedelta(hours=1) 84 | data.update({ 85 | 'decision': OCCURRENCE_DECISIONS['all'], 86 | 'description': 'has changed', 87 | 'start': new_start}) 88 | form = OccurrenceForm(data=data, initial=initial) 89 | self.assertTrue(form.is_valid(), msg=( 90 | 'The OccurrenceForm should be valid')) 91 | form.save() 92 | self.assertEqual(Occurrence.objects.all().count(), 1, msg=( 93 | 'After one occurrence has changed, there should be one persistent' 94 | ' occurrence.')) 95 | occ = Occurrence.objects.get() 96 | self.assertEqual(occ.title, 'different', msg=( 97 | 'When save is called, the occurrence\'s title should still be' 98 | ' "different".')) 99 | self.assertEqual(occ.description, 'has changed', msg=( 100 | 'When save is called, the occurrence\'s description should be' 101 | ' "has changed".')) 102 | self.assertEqual( 103 | occ.start, self.rec_occurrence.start + timedelta(hours=1), msg=( 104 | 'When save is called, the occurrence\'s start time should be' 105 | ' set forward one hour.')) 106 | 107 | # Case 3: Altering everything from occurrence 4 to 6 to one day later 108 | occ_to_use = self.rec_occurrence_list[4] 109 | data = model_to_dict(occ_to_use) 110 | initial = data.copy() 111 | new_start = occ_to_use.start - timedelta(days=1) 112 | data.update({ 113 | 'decision': OCCURRENCE_DECISIONS['following'], 114 | 'start': new_start}) 115 | form = OccurrenceForm(data=data, initial=initial) 116 | self.assertTrue(form.is_valid(), msg=( 117 | 'The OccurrenceForm should be valid')) 118 | form.save() 119 | self.assertEqual(Event.objects.all().count(), 3, msg=( 120 | 'After changing occurrence 4-6, a new event should have been' 121 | ' created.')) 122 | event1 = Event.objects.get(pk=self.rec_event.pk) 123 | event2 = Event.objects.exclude( 124 | pk__in=[self.rec_event.pk, self.event.pk]).get() 125 | self.assertEqual( 126 | event1.end_recurring_period, 127 | event2.start - timedelta(days=1), msg=( 128 | 'The end recurring period of the old event should be the same' 129 | ' as the start of the new event minus one day.')) 130 | self.assertEqual( 131 | event2.end_recurring_period, self.rec_event.end_recurring_period, 132 | msg=( 133 | 'The end recurring period of the new event should be the' 134 | ' old end recurring period of the old event.')) 135 | # -> should yield 2 events, one newly created one altered 136 | -------------------------------------------------------------------------------- /calendarium/tests/models_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the models of the ``calendarium`` app.""" 2 | from django.test import TestCase 3 | from django.utils.timezone import timedelta 4 | from django.template.defaultfilters import slugify 5 | 6 | from mixer.backend.django import mixer 7 | 8 | from ..models import Event, EventCategory, Occurrence, Rule 9 | from ..utils import now 10 | 11 | 12 | class EventModelManagerTestCase(TestCase): 13 | """Tests for the ``EventModelManager`` custom manager.""" 14 | longMessage = True 15 | 16 | def setUp(self): 17 | # event that only occurs once 18 | self.event = mixer.blend('calendarium.Event', rule=None, start=now(), 19 | end=now() + timedelta(hours=1)) 20 | # event that occurs for one week daily with one custom occurrence 21 | self.event_daily = mixer.blend('calendarium.Event') 22 | self.occurrence = mixer.blend( 23 | 'calendarium.Occurrence', event=self.event, original_start=now(), 24 | original_end=now() + timedelta(days=1), title='foo_occurrence') 25 | 26 | def test_get_occurrences(self): 27 | """Test for the ``get_occurrences`` manager method.""" 28 | occurrences = Event.objects.get_occurrences( 29 | now(), now() + timedelta(days=7)) 30 | self.assertEqual(len(occurrences), 1, msg=( 31 | '``get_occurrences`` should return the correct amount of' 32 | ' occurrences.')) 33 | 34 | occurrences = Event.objects.get_occurrences(now(), now()) 35 | self.assertEqual(len(occurrences), 1, msg=( 36 | '``get_occurrences`` should return the correct amount of' 37 | ' occurrences for one day.')) 38 | 39 | 40 | class EventTestCase(TestCase): 41 | """Tests for the ``Event`` model.""" 42 | longMessage = True 43 | 44 | def setUp(self): 45 | self.not_found_event = mixer.blend( 46 | 'calendarium.Event', start=now() - timedelta(hours=24), 47 | end=now() - timedelta(hours=24), 48 | creation_date=now() - timedelta(hours=24), rule=None) 49 | self.event = mixer.blend( 50 | 'calendarium.Event', start=now(), end=now(), 51 | rule__frequency='DAILY', creation_date=now(), 52 | category=mixer.blend('calendarium.EventCategory')) 53 | self.event_wp = mixer.blend( 54 | 'calendarium.Event', start=now(), end=now(), 55 | rule__frequency='DAILY', creation_date=now(), 56 | category=mixer.blend('calendarium.EventCategory'), 57 | end_recurring_period=now() + timedelta(days=2)) 58 | self.occurrence = mixer.blend( 59 | 'calendarium.Occurrence', original_start=now(), 60 | original_end=now() + timedelta(days=1), event=self.event, 61 | title='foo_occurrence') 62 | self.single_time_event = mixer.blend('calendarium.Event', rule=None) 63 | 64 | def test_get_title(self): 65 | """Test for ``__str__`` method.""" 66 | title = "The Title" 67 | event = mixer.blend( 68 | 'calendarium.Event', start=now(), end=now(), 69 | rule__frequency='DAILY', creation_date=now(), 70 | title=title) 71 | self.assertEqual(title, str(event), msg=( 72 | 'Method ``__str__`` did not output event title.')) 73 | 74 | def test_get_absolute_url(self): 75 | """Test for ``get_absolute_url`` method.""" 76 | event = mixer.blend( 77 | 'calendarium.Event', start=now(), end=now(), 78 | rule__frequency='DAILY', creation_date=now()) 79 | event.save() 80 | self.assertTrue(str(event.pk) in str(event.get_absolute_url()), msg=( 81 | 'Method ``get_absolute_url`` did not contain event id.')) 82 | 83 | def test_create_occurrence(self): 84 | """Test for ``_create_occurrence`` method.""" 85 | occurrence = self.event._create_occurrence(now()) 86 | self.assertEqual(type(occurrence), Occurrence, msg=( 87 | 'Method ``_create_occurrence`` did not output the right type.')) 88 | 89 | def test_get_occurrence_gen(self): 90 | """Test for the ``_get_occurrence_gen`` method""" 91 | occurrence_gen = self.event._get_occurrence_gen( 92 | now(), now() + timedelta(days=8)) 93 | occ_list = [occ for occ in occurrence_gen] 94 | self.assertEqual(len(occ_list), 8, msg=( 95 | 'The method ``_get_occurrence_list`` did not return the expected' 96 | ' amount of items.')) 97 | occurrence_gen = self.event_wp._get_occurrence_gen( 98 | now(), now() + timedelta(days=8)) 99 | occ_list = [occ for occ in occurrence_gen] 100 | self.assertEqual(len(occ_list), 2, msg=( 101 | 'The method ``_get_occurrence_list`` did not return the expected' 102 | ' amount of items.')) 103 | occurrence_gen = self.not_found_event._get_occurrence_gen( 104 | now(), now() + timedelta(days=8)) 105 | occ_list = [occ for occ in occurrence_gen] 106 | self.assertEqual(len(occ_list), 0, msg=( 107 | 'The method ``_get_occurrence_list`` did not return the expected' 108 | ' amount of items.')) 109 | 110 | def test_get_occurrences(self): 111 | occurrence_gen = self.event.get_occurrences( 112 | now(), now() + timedelta(days=7)) 113 | occ_list = [occ for occ in occurrence_gen] 114 | self.assertEqual(len(occ_list), 6, msg=( 115 | 'Method ``get_occurrences`` did not output the correct amount' 116 | ' of occurrences.')) 117 | 118 | def test_get_parent_category(self): 119 | """Tests for the ``get_parent_category`` method.""" 120 | result = self.event.get_parent_category() 121 | self.assertEqual(result, self.event.category, msg=( 122 | "If the event's category has no parent, it should return the" 123 | " category")) 124 | 125 | cat2 = mixer.blend('calendarium.EventCategory') 126 | self.event.category.parent = cat2 127 | self.event.save() 128 | result = self.event.get_parent_category() 129 | self.assertEqual(result, self.event.category.parent, msg=( 130 | "If the event's category has a parent, it should return that" 131 | " parent")) 132 | 133 | def test_save_autocorrection(self): 134 | event = mixer.blend( 135 | 'calendarium.Event', rule=None, start=now(), 136 | end=now() + timedelta(hours=1), creation_date=now()) 137 | event.end = event.end - timedelta(hours=2) 138 | event.save() 139 | self.assertEqual(event.start, event.end) 140 | 141 | 142 | class EventCategoryTestCase(TestCase): 143 | """Tests for the ``EventCategory`` model.""" 144 | longMessage = True 145 | 146 | def test_instantiation(self): 147 | """Test for instantiation of the ``EventCategory`` model.""" 148 | event_category = EventCategory() 149 | self.assertTrue(event_category) 150 | 151 | def test_get_name(self): 152 | """Test for ``__str__`` method.""" 153 | name = "The Name" 154 | event_category = EventCategory(name=name) 155 | self.assertEqual(name, str(event_category), msg=( 156 | 'Method ``__str__`` did not output event category name.')) 157 | 158 | def test_get_slug(self): 159 | """Test slug in ``save`` method.""" 160 | name = "The Name" 161 | event_category = EventCategory(name=name) 162 | event_category.save() 163 | self.assertEqual(slugify(name), str(event_category.slug), msg=( 164 | 'Method ``save`` did not set event category slug as expected.')) 165 | 166 | 167 | class EventRelationTestCase(TestCase): 168 | """Tests for the ``EventRelation`` model.""" 169 | longMessage = True 170 | 171 | def test_instantiation(self): 172 | """Test for instantiation of the ``EventRelation`` model.""" 173 | event_relation = mixer.blend('calendarium.EventRelation') 174 | self.assertTrue(event_relation) 175 | 176 | 177 | class OccurrenceTestCase(TestCase): 178 | """Tests for the ``Occurrence`` model.""" 179 | longMessage = True 180 | 181 | def test_instantiation(self): 182 | """Test for instantiation of the ``Occurrence`` model.""" 183 | occurrence = Occurrence() 184 | self.assertTrue(occurrence) 185 | 186 | def test_delete_period(self): 187 | """Test for the ``delete_period`` function.""" 188 | occurrence = mixer.blend('calendarium.Occurrence') 189 | occurrence.delete_period('all') 190 | self.assertEqual(Occurrence.objects.all().count(), 0, msg=( 191 | 'Should delete only the first occurrence.')) 192 | 193 | event = mixer.blend( 194 | 'calendarium.Event', start=now() - timedelta(hours=0), 195 | end=now() - timedelta(hours=0)) 196 | occurrence = mixer.blend( 197 | 'calendarium.Occurrence', event=event, 198 | start=now() - timedelta(hours=0), end=now() - timedelta(hours=0)) 199 | occurrence.delete_period('this one') 200 | self.assertEqual(Occurrence.objects.all().count(), 0, msg=( 201 | 'Should delete only the first occurrence.')) 202 | 203 | event = mixer.blend( 204 | 'calendarium.Event', start=now() - timedelta(hours=0), 205 | end=now() - timedelta(hours=0)) 206 | event.save() 207 | occurrence = mixer.blend( 208 | 'calendarium.Occurrence', event=event, 209 | start=now() - timedelta(hours=0), end=now() - timedelta(hours=0)) 210 | occurrence.delete_period('following') 211 | self.assertEqual(Event.objects.all().count(), 0, msg=( 212 | 'Should delete the event and the occurrence.')) 213 | 214 | occurrence_1 = mixer.blend( 215 | 'calendarium.Occurrence', start=now(), 216 | end=now() + timedelta(days=1), 217 | original_start=now() + timedelta(hours=1)) 218 | occurrence_2 = mixer.blend( 219 | 'calendarium.Occurrence', start=now(), 220 | end=now() + timedelta(days=1), 221 | original_start=now() + timedelta(hours=1)) 222 | occurrence_2.event = occurrence_1.event 223 | occurrence_2.save() 224 | occurrence_2.delete_period('this one') 225 | # Result is equal instead of greater. Needs to be fixed. 226 | # self.assertGreater(period, occurrence_2.event.end_recurring_period, 227 | # msg=('Should shorten event period, if last' 228 | # ' occurencce is deleted.')) 229 | 230 | occurrence_3 = mixer.blend( 231 | 'calendarium.Occurrence', start=now(), 232 | end=now() + timedelta(days=1), 233 | original_start=now() + timedelta(hours=1)) 234 | occurrence_3.event = occurrence_1.event 235 | occurrence_3.save() 236 | occurrence_4 = mixer.blend( 237 | 'calendarium.Occurrence', start=now(), 238 | end=now() + timedelta(days=1), 239 | original_start=now() + timedelta(hours=1)) 240 | occurrence_4.event = occurrence_1.event 241 | occurrence_4.save() 242 | occurrence_3.delete_period('this one') 243 | occurrence_1.delete_period('following') 244 | self.assertEqual(Occurrence.objects.all().count(), 0, msg=( 245 | 'Should delete all occurrences with this start date.')) 246 | 247 | 248 | class RuleTestCase(TestCase): 249 | """Tests for the ``Rule`` model.""" 250 | longMessage = True 251 | 252 | def test_instantiation(self): 253 | """Test for instantiation of the ``Rule`` model.""" 254 | rule = Rule() 255 | self.assertTrue(rule) 256 | -------------------------------------------------------------------------------- /calendarium/tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | These settings are used by the ``manage.py`` command. 3 | With normal tests we want to use the fastest possible way which is an 4 | in-memory sqlite database but if you want to create South migrations you 5 | need a persistant database. 6 | Unfortunately there seems to be an issue with either South or syncdb so that 7 | defining two routers ("default" and "south") does not work. 8 | """ 9 | from distutils.version import StrictVersion 10 | 11 | import django 12 | 13 | from .test_settings import * # NOQA 14 | 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', 19 | 'NAME': 'db.sqlite', 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /calendarium/tests/tags_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the template tags of the ``calendarium`` app.""" 2 | from django.template import Context, Template 3 | from django.test import TestCase 4 | from django.utils import timezone 5 | 6 | from mixer.backend.django import mixer 7 | 8 | from ..templatetags.calendarium_tags import get_upcoming_events, get_week_URL 9 | 10 | 11 | class RenderUpcomingEventsTestCase(TestCase): 12 | """Tests for the ``render_upcoming_events`` tag.""" 13 | longMessage = True 14 | 15 | def setUp(self): 16 | self.occurrence = mixer.blend( 17 | 'calendarium.Occurrence', 18 | start=timezone.now() + timezone.timedelta(days=1), 19 | end=timezone.now() + timezone.timedelta(days=2), 20 | original_start=timezone.now() + timezone.timedelta(seconds=20), 21 | event__start=timezone.now() + timezone.timedelta(days=1), 22 | event__end=timezone.now() + timezone.timedelta(days=2), 23 | event__title='foo', 24 | ) 25 | 26 | def test_render_tag(self): 27 | t = Template('{% load calendarium_tags %}{% render_upcoming_events %}') 28 | self.assertIn('foo', t.render(Context())) 29 | 30 | 31 | class GetUpcomingEventsTestCase(TestCase): 32 | """Tests for the ``get_upcoming_events`` tag.""" 33 | longMessage = True 34 | 35 | def setUp(self): 36 | self.occurrence = mixer.blend( 37 | 'calendarium.Occurrence', 38 | start=timezone.now() + timezone.timedelta(days=1), 39 | end=timezone.now() + timezone.timedelta(days=2), 40 | original_start=timezone.now() + timezone.timedelta(seconds=20), 41 | event__start=timezone.now() + timezone.timedelta(days=1), 42 | event__end=timezone.now() + timezone.timedelta(days=2), 43 | ) 44 | 45 | def test_tag(self): 46 | result = get_upcoming_events() 47 | self.assertEqual(len(result), 1) 48 | 49 | 50 | class GetWeekURLTestCase(TestCase): 51 | """Tests for the ``get_week_URL`` tag.""" 52 | longMessage = True 53 | 54 | def test_tag(self): 55 | result = get_week_URL( 56 | timezone.datetime.strptime('2016-02-07', '%Y-%m-%d')) 57 | self.assertEqual(result, u'/2016/week/5/') 58 | -------------------------------------------------------------------------------- /calendarium/tests/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/tests/test_app/__init__.py -------------------------------------------------------------------------------- /calendarium/tests/test_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='DummyModel', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('content', models.CharField(max_length=32)), 18 | ], 19 | options={ 20 | }, 21 | bases=(models.Model,), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /calendarium/tests/test_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/tests/test_app/migrations/__init__.py -------------------------------------------------------------------------------- /calendarium/tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | """Models for the ``test_app`` test app.""" 2 | from django.db import models 3 | 4 | 5 | class DummyModel(models.Model): 6 | """ 7 | This is a dummy model for testing purposes. 8 | 9 | :content: Just a dummy field. 10 | 11 | """ 12 | content = models.CharField( 13 | max_length=32, 14 | ) 15 | -------------------------------------------------------------------------------- /calendarium/tests/test_app/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | from django.db import models 7 | 8 | 9 | class Migration(SchemaMigration): 10 | 11 | def forwards(self, orm): 12 | # Adding model 'DummyModel' 13 | db.create_table('test_app_dummymodel', ( 14 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('content', self.gf('django.db.models.fields.CharField')(max_length=32)), 16 | )) 17 | db.send_create_signal('test_app', ['DummyModel']) 18 | 19 | 20 | def backwards(self, orm): 21 | # Deleting model 'DummyModel' 22 | db.delete_table('test_app_dummymodel') 23 | 24 | 25 | models = { 26 | 'test_app.dummymodel': { 27 | 'Meta': {'object_name': 'DummyModel'}, 28 | 'content': ('django.db.models.fields.CharField', [], {'max_length': '32'}), 29 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 30 | } 31 | } 32 | 33 | complete_apps = ['test_app'] -------------------------------------------------------------------------------- /calendarium/tests/test_app/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitlabstudio/django-calendarium/da948cbcccf1df40c631fb14d9b869c74650ee2d/calendarium/tests/test_app/south_migrations/__init__.py -------------------------------------------------------------------------------- /calendarium/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | """Settings that need to be set in order to run the tests.""" 2 | import os 3 | 4 | DEBUG = True 5 | USE_TZ = True 6 | TIME_ZONE = 'Asia/Singapore' 7 | 8 | 9 | EXTERNAL_APPS = [ 10 | 'django.contrib.admin', 11 | 'django.contrib.admindocs', 12 | 'django.contrib.auth', 13 | 'django.contrib.contenttypes', 14 | 'django.contrib.messages', 15 | 'django.contrib.sessions', 16 | 'django.contrib.staticfiles', 17 | 'django.contrib.sitemaps', 18 | 'django.contrib.sites', 19 | 'easy_thumbnails', 20 | 'filer', 21 | 'django_libs', 22 | ] 23 | 24 | INTERNAL_APPS = [ 25 | 'calendarium', 26 | ] 27 | 28 | TEST_APPS = [ 29 | 'calendarium.tests.test_app', 30 | ] 31 | 32 | INSTALLED_APPS = EXTERNAL_APPS + INTERNAL_APPS + TEST_APPS 33 | 34 | SITE_ID = 1 35 | 36 | SECRET_KEY = 'Foobar' 37 | ALLOWED_HOSTS = [] 38 | 39 | DATABASES = { 40 | "default": { 41 | "ENGINE": "django.db.backends.sqlite3", 42 | "NAME": ":memory:", 43 | } 44 | } 45 | 46 | ROOT_URLCONF = 'calendarium.tests.urls' 47 | 48 | STATIC_URL = '/static/' 49 | 50 | STATIC_ROOT = os.path.join(__file__, '../../static/') 51 | 52 | STATICFILES_DIRS = ( 53 | os.path.join(__file__, 'test_static'), 54 | ) 55 | 56 | TEMPLATES = [{ 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'APP_DIRS': True, 59 | 'DIRS': [os.path.join(os.path.dirname(__file__), '../templates')], 60 | 'OPTIONS': { 61 | 'context_processors': ( 62 | 'django.contrib.auth.context_processors.auth', 63 | 'django.template.context_processors.i18n', 64 | 'django.contrib.messages.context_processors.messages', 65 | 'django.template.context_processors.request', 66 | 'django.template.context_processors.media', 67 | 'django.template.context_processors.static', 68 | ) 69 | } 70 | }] 71 | 72 | # Django 1.8 compatibility 73 | MIDDLEWARE = [ 74 | 'django.contrib.sessions.middleware.SessionMiddleware', 75 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 76 | 'django.contrib.messages.middleware.MessageMiddleware', 77 | ] 78 | 79 | -------------------------------------------------------------------------------- /calendarium/tests/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | This ``urls.py`` is only used when running the tests via ``runtests.py``. 3 | As you know, every app must be hooked into yout main ``urls.py`` so that 4 | you can actually reach the app's views (provided it has any views, of course). 5 | 6 | """ 7 | from django.conf.urls import include, url 8 | from django.urls import path 9 | from django.contrib import admin 10 | 11 | 12 | admin.autodiscover() 13 | 14 | 15 | urlpatterns = [ 16 | path('admin/', admin.site.urls), 17 | url(r'^', include('calendarium.urls')), 18 | ] 19 | -------------------------------------------------------------------------------- /calendarium/tests/views_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for the views of the ``calendarium`` app.""" 2 | # ! Never use the timezone now, import calendarium.utils.now instead always 3 | # inaccuracy on microsecond base can negatively influence your tests 4 | # from django.utils.timezone import now 5 | from django.utils.timezone import timedelta 6 | from django.test import TestCase 7 | 8 | from django_libs.tests.mixins import ViewRequestFactoryTestMixin 9 | from mixer.backend.django import mixer 10 | 11 | from .. import views 12 | from ..models import Event 13 | from ..utils import now 14 | 15 | 16 | class CalendariumRedirectViewTestCase(ViewRequestFactoryTestMixin, TestCase): 17 | """Tests for the ``CalendariumRedirectView`` view.""" 18 | view_class = views.CalendariumRedirectView 19 | 20 | def test_view(self): 21 | resp = self.client.get(self.get_url()) 22 | self.assertEqual(resp.status_code, 302) 23 | 24 | 25 | class MonthViewTestCase(ViewRequestFactoryTestMixin, TestCase): 26 | """Tests for the ``MonthView`` view class.""" 27 | view_class = views.MonthView 28 | 29 | def get_view_kwargs(self): 30 | return {'year': self.year, 'month': self.month} 31 | 32 | def setUp(self): 33 | self.year = now().year 34 | self.month = now().month 35 | 36 | def test_view(self): 37 | """Test for the ``MonthView`` view class.""" 38 | # regular call 39 | resp = self.is_callable() 40 | self.assertEqual( 41 | resp.template_name[0], 'calendarium/calendar_month.html', msg=( 42 | 'Returned the wrong template.')) 43 | self.is_postable(data={'next': True}, to_url_name='calendar_month') 44 | self.is_postable(data={'previous': True}, to_url_name='calendar_month') 45 | self.is_postable(data={'today': True}, to_url_name='calendar_month') 46 | 47 | # called with a invalid category pk 48 | self.is_callable(data={'category': 'abc'}) 49 | 50 | # called with a non-existant category pk 51 | self.is_callable(data={'category': '999'}) 52 | 53 | # called with a category pk 54 | category = mixer.blend('calendarium.EventCategory') 55 | self.is_callable(data={'category': category.pk}) 56 | 57 | # called with wrong values 58 | self.is_not_callable(kwargs={'year': 2000, 'month': 15}) 59 | 60 | 61 | class WeekViewTestCase(ViewRequestFactoryTestMixin, TestCase): 62 | """Tests for the ``WeekView`` view class.""" 63 | view_class = views.WeekView 64 | 65 | def get_view_kwargs(self): 66 | return {'year': self.year, 'week': self.week} 67 | 68 | def setUp(self): 69 | self.year = now().year 70 | # current week number 71 | self.week = now().date().isocalendar()[1] 72 | 73 | def test_view(self): 74 | """Tests for the ``WeekView`` view class.""" 75 | resp = self.is_callable() 76 | self.assertEqual( 77 | resp.template_name[0], 'calendarium/calendar_week.html', msg=( 78 | 'Returned the wrong template.')) 79 | self.is_postable(data={'next': True}, to_url_name='calendar_week') 80 | self.is_postable(data={'previous': True}, to_url_name='calendar_week') 81 | self.is_postable(data={'today': True}, to_url_name='calendar_week') 82 | 83 | resp = self.is_callable(ajax=True) 84 | self.assertEqual( 85 | resp.template_name[0], 'calendarium/partials/calendar_week.html', 86 | msg=('Returned the wrong template for AJAX request.')) 87 | self.is_not_callable(kwargs={'year': self.year, 'week': '60'}) 88 | 89 | 90 | class DayViewTestCase(ViewRequestFactoryTestMixin, TestCase): 91 | """Tests for the ``DayView`` view class.""" 92 | view_class = views.DayView 93 | 94 | def get_view_kwargs(self): 95 | return {'year': self.year, 'month': self.month, 'day': self.day} 96 | 97 | def setUp(self): 98 | self.year = 2001 99 | self.month = 2 100 | self.day = 15 101 | 102 | def test_view(self): 103 | """Tests for the ``DayView`` view class.""" 104 | resp = self.is_callable() 105 | self.assertEqual( 106 | resp.template_name[0], 'calendarium/calendar_day.html', msg=( 107 | 'Returned the wrong template.')) 108 | self.is_postable(data={'next': True}, to_url_name='calendar_day') 109 | self.is_postable(data={'previous': True}, to_url_name='calendar_day') 110 | self.is_postable(data={'today': True}, to_url_name='calendar_day') 111 | self.is_not_callable(kwargs={'year': self.year, 'month': '14', 112 | 'day': self.day}) 113 | 114 | 115 | class EventUpdateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 116 | """Tests for the ``EventUpdateView`` view class.""" 117 | view_class = views.EventUpdateView 118 | 119 | def get_view_kwargs(self): 120 | return {'pk': self.event.pk} 121 | 122 | def setUp(self): 123 | self.event = mixer.blend('calendarium.Event') 124 | self.user = mixer.blend('auth.User', is_superuser=True) 125 | 126 | def test_view(self): 127 | self.is_callable(user=self.user) 128 | 129 | 130 | class EventCreateViewTestCase(ViewRequestFactoryTestMixin, TestCase): 131 | """Tests for the ``EventCreateView`` view class.""" 132 | view_class = views.EventCreateView 133 | 134 | def setUp(self): 135 | self.user = mixer.blend('auth.User', is_superuser=True) 136 | 137 | def test_view(self): 138 | self.is_callable(user=self.user) 139 | self.is_callable(user=self.user, data={'delete': True}) 140 | self.assertEqual(Event.objects.all().count(), 0) 141 | 142 | 143 | class EventDetailViewTestCase(ViewRequestFactoryTestMixin, TestCase): 144 | """Tests for the ``EventDetailView`` view class.""" 145 | view_class = views.EventDetailView 146 | 147 | def get_view_kwargs(self): 148 | return {'pk': self.event.pk} 149 | 150 | def setUp(self): 151 | self.event = mixer.blend('calendarium.Event') 152 | 153 | def test_view(self): 154 | self.is_callable() 155 | 156 | 157 | class OccurrenceViewTestCaseMixin(object): 158 | """Mixin to avoid repeating code for the Occurrence views.""" 159 | def get_view_kwargs(self): 160 | return { 161 | 'pk': self.event.pk, 162 | 'year': self.event.start.date().year, 163 | 'month': self.event.start.date().month, 164 | 'day': self.event.start.date().day, 165 | } 166 | 167 | def setUp(self): 168 | self.rule = mixer.blend('calendarium.Rule', name='daily') 169 | self.event = mixer.blend( 170 | 'calendarium.Event', created_by=mixer.blend('auth.User'), 171 | start=now() - timedelta(days=1), end=now() + timedelta(days=5), 172 | rule=self.rule, end_recurring_period=now() + timedelta(days=2)) 173 | 174 | 175 | class OccurrenceDeleteViewTestCase( 176 | OccurrenceViewTestCaseMixin, ViewRequestFactoryTestMixin, TestCase): 177 | """Tests for the ``OccurrenceDeleteView`` view class.""" 178 | view_class = views.OccurrenceDeleteView 179 | 180 | def test_deletion(self): 181 | self.is_not_callable(kwargs={ 182 | 'pk': 5, 183 | 'year': self.event.start.date().year, 184 | 'month': self.event.start.date().month, 185 | 'day': self.event.start.date().day, 186 | }, user=self.event.created_by, msg=('Wrong event pk.')) 187 | 188 | self.is_not_callable(kwargs={ 189 | 'pk': self.event.pk, 190 | 'year': self.event.start.date().year, 191 | 'month': '999', 192 | 'day': self.event.start.date().day, 193 | }, user=self.event.created_by, msg=('Wrong dates.')) 194 | 195 | new_rule = mixer.blend('calendarium.Rule', name='weekly', 196 | frequency='WEEKLY') 197 | new_event = mixer.blend( 198 | 'calendarium.Event', 199 | rule=new_rule, 200 | end_recurring_period=now() + timedelta(days=200), 201 | start=now() - timedelta(hours=5), 202 | ) 203 | test_date = self.event.start.date() - timedelta(days=5) 204 | self.is_not_callable(kwargs={ 205 | 'pk': new_event.pk, 206 | 'year': test_date.year, 207 | 'month': test_date.month, 208 | 'day': test_date.day, 209 | }, user=self.event.created_by, msg=( 210 | 'No occurrence available for this day.')) 211 | 212 | self.is_callable(user=self.event.created_by) 213 | self.is_postable(user=self.event.created_by, to='/', 214 | data={'decision': 'this one'}) 215 | 216 | 217 | class OccurrenceDetailViewTestCase( 218 | OccurrenceViewTestCaseMixin, ViewRequestFactoryTestMixin, TestCase): 219 | """Tests for the ``OccurrenceDetailView`` view class.""" 220 | view_class = views.OccurrenceDetailView 221 | 222 | 223 | class OccurrenceUpdateViewTestCase( 224 | OccurrenceViewTestCaseMixin, ViewRequestFactoryTestMixin, TestCase): 225 | """Tests for the ``OccurrenceUpdateView`` view class.""" 226 | view_class = views.OccurrenceUpdateView 227 | 228 | 229 | class UpcomingEventsAjaxViewTestCase(ViewRequestFactoryTestMixin, TestCase): 230 | """Tests for the ``UpcomingEventsAjaxView`` view class.""" 231 | view_class = views.UpcomingEventsAjaxView 232 | 233 | def test_view(self): 234 | self.is_callable() 235 | 236 | def test_view_with_count(self): 237 | self.is_callable(data={'count': 5}) 238 | 239 | def test_view_with_category(self): 240 | cat = mixer.blend('calendarium.EventCategory') 241 | self.is_callable(data={'category': cat.slug}) 242 | -------------------------------------------------------------------------------- /calendarium/urls.py: -------------------------------------------------------------------------------- 1 | """URLs for the ``calendarium`` app.""" 2 | from django.conf.urls import url 3 | 4 | from . import views 5 | 6 | 7 | urlpatterns = [ 8 | # event views 9 | url(r'^event/create/$', 10 | views.EventCreateView.as_view(), 11 | name='calendar_event_create'), 12 | 13 | url(r'^event/(?P\d+)/$', 14 | views.EventDetailView.as_view(), 15 | name='calendar_event_detail'), 16 | 17 | url(r'^event/(?P\d+)/update/$', 18 | views.EventUpdateView.as_view(), 19 | name='calendar_event_update'), 20 | 21 | url(r'^event/(?P\d+)/delete/$', 22 | views.EventDeleteView.as_view(), 23 | name='calendar_event_delete'), 24 | 25 | # occurrence views 26 | url(r'^event/(?P\d+)/date/(?P\d+)/(?P\d+)/(?P\d+)/$', 27 | views.OccurrenceDetailView.as_view(), 28 | name='calendar_occurrence_detail'), 29 | 30 | url( 31 | r'^event/(?P\d+)/date/(?P\d+)/(?P\d+)/(?P\d+)/update/$', # NOPEP8 32 | views.OccurrenceUpdateView.as_view(), 33 | name='calendar_occurrence_update'), 34 | 35 | url( 36 | r'^event/(?P\d+)/date/(?P\d+)/(?P\d+)/(?P\d+)/delete/$', # NOPEP8 37 | views.OccurrenceDeleteView.as_view(), 38 | name='calendar_occurrence_delete'), 39 | 40 | # calendar views 41 | url(r'^(?P\d+)/(?P\d+)/$', 42 | views.MonthView.as_view(), 43 | name='calendar_month'), 44 | 45 | url(r'^(?P\d+)/week/(?P\d+)/$', 46 | views.WeekView.as_view(), 47 | name='calendar_week'), 48 | 49 | url(r'^(?P\d+)/(?P\d+)/(?P\d+)/$', 50 | views.DayView.as_view(), 51 | name='calendar_day'), 52 | 53 | url(r'^get-events/$', 54 | views.UpcomingEventsAjaxView.as_view(), 55 | name='calendar_upcoming_events'), 56 | 57 | url(r'^$', 58 | views.CalendariumRedirectView.as_view(), 59 | name='calendar_current_month'), 60 | 61 | ] 62 | -------------------------------------------------------------------------------- /calendarium/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utils for the ``calendarium`` app. 3 | 4 | The code of these utils is highly influenced by or taken from the utils of 5 | django-schedule: 6 | 7 | https://github.com/thauber/django-schedule/blob/master/schedule/utils.py 8 | 9 | 10 | """ 11 | import time 12 | from django.utils import timezone 13 | 14 | 15 | def now(**kwargs): 16 | """ 17 | Utility function to zero microseconds to avoid inaccuracy. 18 | 19 | I replaced the microseconds, because there is some slightly varying 20 | difference that occurs out of unknown reason. Since we probably never 21 | schedule events on microsecond basis, seconds and microseconds will be 22 | zeroed everywhere. 23 | 24 | """ 25 | return timezone.now(**kwargs).replace(second=0, microsecond=0) 26 | 27 | 28 | def monday_of_week(year, week): 29 | """ 30 | Returns a datetime for the monday of the given week of the given year. 31 | 32 | """ 33 | str_time = time.strptime('{0} {1} 1'.format(year, week), '%Y %W %w') 34 | date = timezone.datetime(year=str_time.tm_year, month=str_time.tm_mon, 35 | day=str_time.tm_mday, tzinfo=timezone.utc) 36 | if timezone.datetime(year, 1, 4).isoweekday() > 4: 37 | # ISO 8601 where week 1 is the first week that has at least 4 days in 38 | # the current year 39 | date -= timezone.timedelta(days=7) 40 | return date 41 | 42 | 43 | class OccurrenceReplacer(object): 44 | """ 45 | When getting a list of occurrences, the last thing that needs to be done 46 | before passing it forward is to make sure all of the occurrences that 47 | have been stored in the datebase replace, in the list you are returning, 48 | the generated ones that are equivalent. This class makes this easier. 49 | 50 | """ 51 | def __init__(self, persisted_occurrences): 52 | lookup = [ 53 | ((occ.event, occ.original_start, occ.original_end), occ) for 54 | occ in persisted_occurrences] 55 | self.lookup = dict(lookup) 56 | 57 | def get_occurrence(self, occ): 58 | """ 59 | Return a persisted occurrences matching the occ and remove it from 60 | lookup since it has already been matched 61 | """ 62 | return self.lookup.pop( 63 | (occ.event, occ.original_start, occ.original_end), 64 | occ) 65 | 66 | def has_occurrence(self, occ): 67 | return (occ.event, occ.original_start, occ.original_end) in self.lookup 68 | 69 | def get_additional_occurrences(self, start, end): 70 | """ 71 | Return persisted occurrences which are now in the period 72 | """ 73 | return [occ for key, occ in self.lookup.items() if ( 74 | (end and occ.start < end) and 75 | occ.end >= start and not occ.cancelled)] 76 | -------------------------------------------------------------------------------- /calendarium/views.py: -------------------------------------------------------------------------------- 1 | """Views for the ``calendarium`` app.""" 2 | import calendar 3 | from dateutil.relativedelta import relativedelta 4 | 5 | from django.contrib.auth.decorators import permission_required 6 | from django.urls import reverse 7 | from django.forms.models import model_to_dict 8 | from django.http import Http404, HttpResponseRedirect 9 | from django.utils.decorators import method_decorator 10 | from django.utils.timezone import datetime, now, timedelta, utc 11 | from django.utils.translation import ugettext_lazy as _ 12 | from django.views.generic import ( 13 | CreateView, 14 | DeleteView, 15 | DetailView, 16 | ListView, 17 | RedirectView, 18 | TemplateView, 19 | UpdateView, 20 | ) 21 | 22 | from .constants import OCCURRENCE_DECISIONS 23 | from .forms import OccurrenceForm 24 | from .models import EventCategory, Event, Occurrence 25 | from .settings import SHIFT_WEEKSTART 26 | from .utils import monday_of_week 27 | 28 | 29 | class CategoryMixin(object): 30 | """Mixin to handle category filtering by category id.""" 31 | def dispatch(self, request, *args, **kwargs): 32 | if request.GET.get('category'): 33 | try: 34 | category_id = int(request.GET.get('category')) 35 | except ValueError: 36 | pass 37 | else: 38 | try: 39 | self.category = EventCategory.objects.get(pk=category_id) 40 | except EventCategory.DoesNotExist: 41 | pass 42 | return super(CategoryMixin, self).dispatch(request, *args, **kwargs) 43 | 44 | def get_category_context(self, **kwargs): 45 | context = {'categories': EventCategory.objects.all()} 46 | if hasattr(self, 'category'): 47 | context.update({'current_category': self.category}) 48 | return context 49 | 50 | 51 | class CalendariumRedirectView(RedirectView): 52 | """View to redirect to the current month view.""" 53 | permanent = False 54 | 55 | def get_redirect_url(self, **kwargs): 56 | return reverse('calendar_month', kwargs={'year': now().year, 57 | 'month': now().month}) 58 | 59 | 60 | class MonthView(CategoryMixin, TemplateView): 61 | """View to return all occurrences of an event for a whole month.""" 62 | template_name = 'calendarium/calendar_month.html' 63 | 64 | def dispatch(self, request, *args, **kwargs): 65 | self.month = int(kwargs.get('month')) 66 | self.year = int(kwargs.get('year')) 67 | if self.month not in range(1, 13): 68 | raise Http404 69 | if request.method == 'POST': 70 | if request.POST.get('next'): 71 | new_date = datetime(self.year, self.month, 1) + timedelta( 72 | days=31) 73 | kwargs.update({'year': new_date.year, 'month': new_date.month}) 74 | return HttpResponseRedirect( 75 | reverse('calendar_month', kwargs=kwargs)) 76 | elif request.POST.get('previous'): 77 | new_date = datetime(self.year, self.month, 1) - timedelta( 78 | days=1) 79 | kwargs.update({'year': new_date.year, 'month': new_date.month}) 80 | return HttpResponseRedirect( 81 | reverse('calendar_month', kwargs=kwargs)) 82 | elif request.POST.get('today'): 83 | kwargs.update({'year': now().year, 'month': now().month}) 84 | return HttpResponseRedirect( 85 | reverse('calendar_month', kwargs=kwargs)) 86 | if request.is_ajax(): 87 | self.template_name = 'calendarium/partials/calendar_month.html' 88 | return super(MonthView, self).dispatch(request, *args, **kwargs) 89 | 90 | def get_context_data(self, **kwargs): 91 | firstweekday = 0 + SHIFT_WEEKSTART 92 | while firstweekday < 0: 93 | firstweekday += 7 94 | while firstweekday > 6: 95 | firstweekday -= 7 96 | 97 | ctx = self.get_category_context() 98 | month = [[]] 99 | week = 0 100 | start = datetime(year=self.year, month=self.month, day=1, tzinfo=utc) 101 | end = datetime( 102 | year=self.year, month=self.month, day=1, tzinfo=utc 103 | ) + relativedelta(months=1) 104 | 105 | all_occurrences = Event.objects.get_occurrences( 106 | start, end, ctx.get('current_category')) 107 | cal = calendar.Calendar() 108 | cal.setfirstweekday(firstweekday) 109 | for day in cal.itermonthdays(self.year, self.month): 110 | current = False 111 | if day: 112 | date = datetime(year=self.year, month=self.month, day=day, 113 | tzinfo=utc) 114 | occurrences = filter( 115 | lambda occ, date=date: occ.start.replace( 116 | hour=0, minute=0, second=0, microsecond=0) == date, 117 | all_occurrences) 118 | if date.date() == now().date(): 119 | current = True 120 | else: 121 | occurrences = [] 122 | month[week].append((day, occurrences, current)) 123 | if len(month[week]) == 7: 124 | month.append([]) 125 | week += 1 126 | calendar.setfirstweekday(firstweekday) 127 | weekdays = [_(header) for header in calendar.weekheader(10).split()] 128 | ctx.update({'month': month, 'date': date, 'weekdays': weekdays}) 129 | return ctx 130 | 131 | 132 | class WeekView(CategoryMixin, TemplateView): 133 | """View to return all occurrences of an event for one week.""" 134 | template_name = 'calendarium/calendar_week.html' 135 | 136 | def dispatch(self, request, *args, **kwargs): 137 | self.week = int(kwargs.get('week')) 138 | self.year = int(kwargs.get('year')) 139 | if self.week not in range(1, 53): 140 | raise Http404 141 | if request.method == 'POST': 142 | if request.POST.get('next'): 143 | date = monday_of_week(self.year, self.week) + timedelta(days=7) 144 | kwargs.update( 145 | {'year': date.year, 'week': date.date().isocalendar()[1]}) 146 | return HttpResponseRedirect( 147 | reverse('calendar_week', kwargs=kwargs)) 148 | elif request.POST.get('previous'): 149 | date = monday_of_week(self.year, self.week) - timedelta(days=7) 150 | kwargs.update( 151 | {'year': date.year, 'week': date.date().isocalendar()[1]}) 152 | return HttpResponseRedirect( 153 | reverse('calendar_week', kwargs=kwargs)) 154 | elif request.POST.get('today'): 155 | kwargs.update({ 156 | 'year': now().year, 157 | 'week': now().date().isocalendar()[1], 158 | }) 159 | return HttpResponseRedirect( 160 | reverse('calendar_week', kwargs=kwargs)) 161 | if request.is_ajax(): 162 | self.template_name = 'calendarium/partials/calendar_week.html' 163 | return super(WeekView, self).dispatch(request, *args, **kwargs) 164 | 165 | def get_context_data(self, **kwargs): 166 | ctx = self.get_category_context() 167 | date = monday_of_week(self.year, self.week) + relativedelta( 168 | days=SHIFT_WEEKSTART) 169 | week = [] 170 | day = SHIFT_WEEKSTART 171 | start = date 172 | end = date + relativedelta(days=7 + SHIFT_WEEKSTART) 173 | all_occurrences = Event.objects.get_occurrences( 174 | start, end, ctx.get('current_category')) 175 | while day < 7 + SHIFT_WEEKSTART: 176 | current = False 177 | occurrences = filter( 178 | lambda occ, date=date: occ.start.replace( 179 | hour=0, minute=0, second=0, microsecond=0) == date, 180 | all_occurrences) 181 | if date.date() == now().date(): 182 | current = True 183 | week.append((date, occurrences, current)) 184 | day += 1 185 | date = date + timedelta(days=1) 186 | ctx.update({'week': week, 'date': date, 'week_nr': self.week}) 187 | return ctx 188 | 189 | 190 | class DayView(CategoryMixin, TemplateView): 191 | """View to return all occurrences of an event for one day.""" 192 | template_name = 'calendarium/calendar_day.html' 193 | 194 | def dispatch(self, request, *args, **kwargs): 195 | self.day = int(kwargs.get('day')) 196 | self.month = int(kwargs.get('month')) 197 | self.year = int(kwargs.get('year')) 198 | try: 199 | self.date = datetime(year=self.year, month=self.month, 200 | day=self.day, tzinfo=utc) 201 | except ValueError: 202 | raise Http404 203 | if request.method == 'POST': 204 | if request.POST.get('next'): 205 | date = self.date + timedelta(days=1) 206 | kwargs.update( 207 | {'year': date.year, 'month': date.month, 'day': date.day}) 208 | return HttpResponseRedirect( 209 | reverse('calendar_day', kwargs=kwargs)) 210 | elif request.POST.get('previous'): 211 | date = self.date - timedelta(days=1) 212 | kwargs.update({ 213 | 'year': date.year, 214 | 'month': date.month, 215 | 'day': date.day, 216 | }) 217 | return HttpResponseRedirect( 218 | reverse('calendar_day', kwargs=kwargs)) 219 | elif request.POST.get('today'): 220 | kwargs.update({ 221 | 'year': now().year, 222 | 'month': now().month, 223 | 'day': now().day, 224 | }) 225 | return HttpResponseRedirect( 226 | reverse('calendar_day', kwargs=kwargs)) 227 | if request.is_ajax(): 228 | self.template_name = 'calendarium/partials/calendar_day.html' 229 | return super(DayView, self).dispatch(request, *args, **kwargs) 230 | 231 | def get_context_data(self, **kwargs): 232 | ctx = self.get_category_context() 233 | occurrences = Event.objects.get_occurrences( 234 | self.date, self.date, ctx.get('current_category')) 235 | ctx.update({ 236 | 'date': self.date, 237 | 'occurrences': filter( 238 | lambda occ, date=self.date: occ.start.replace( 239 | hour=0, minute=0, second=0, microsecond=0) == self.date, 240 | occurrences), 241 | }) 242 | return ctx 243 | 244 | 245 | class EventDetailView(DetailView): 246 | """View to return information of an event.""" 247 | model = Event 248 | 249 | 250 | class EventMixin(object): 251 | """Mixin to handle event-related functions.""" 252 | model = Event 253 | fields = '__all__' 254 | 255 | @method_decorator(permission_required('calendarium.add_event')) 256 | def dispatch(self, request, *args, **kwargs): 257 | return super(EventMixin, self).dispatch(request, *args, **kwargs) 258 | 259 | def get_success_url(self): 260 | return reverse('calendar_event_detail', kwargs={'pk': self.object.pk}) 261 | 262 | 263 | class EventUpdateView(EventMixin, UpdateView): 264 | """View to update information of an event.""" 265 | pass 266 | 267 | 268 | class EventCreateView(EventMixin, CreateView): 269 | """View to create an event.""" 270 | pass 271 | 272 | 273 | class EventDeleteView(EventMixin, DeleteView): 274 | """View to delete an event.""" 275 | def get_success_url(self): 276 | return reverse('calendar_current_month') 277 | 278 | 279 | class OccurrenceViewMixin(object): 280 | """Mixin to avoid repeating code for the Occurrence view classes.""" 281 | form_class = OccurrenceForm 282 | 283 | def dispatch(self, request, *args, **kwargs): 284 | try: 285 | self.event = Event.objects.get(pk=kwargs.get('pk')) 286 | except Event.DoesNotExist: 287 | raise Http404 288 | year = int(kwargs.get('year')) 289 | month = int(kwargs.get('month')) 290 | day = int(kwargs.get('day')) 291 | try: 292 | date = datetime(year, month, day, tzinfo=utc) 293 | except (TypeError, ValueError): 294 | raise Http404 295 | # this should retrieve the one single occurrence, that has a 296 | # matching start date 297 | try: 298 | occ = Occurrence.objects.get( 299 | start__year=year, start__month=month, start__day=day) 300 | except Occurrence.DoesNotExist: 301 | occ_gen = self.event.get_occurrences(self.event.start) 302 | occ = next(occ_gen) 303 | while occ.start.date() < date.date(): 304 | occ = next(occ_gen) 305 | if occ.start.date() == date.date(): 306 | self.occurrence = occ 307 | else: 308 | raise Http404 309 | self.object = occ 310 | return super(OccurrenceViewMixin, self).dispatch( 311 | request, *args, **kwargs) 312 | 313 | def get_object(self): 314 | return self.object 315 | 316 | def get_form_kwargs(self): 317 | kwargs = super(OccurrenceViewMixin, self).get_form_kwargs() 318 | kwargs.update({'initial': model_to_dict(self.object)}) 319 | return kwargs 320 | 321 | def get_success_url(self): 322 | return reverse('calendar_occurrence_update', kwargs={ 323 | 'pk': self.object.event.pk, 324 | 'year': self.object.start.year, 325 | 'month': self.object.start.month, 326 | 'day': self.object.start.day, 327 | }) 328 | 329 | 330 | class OccurrenceDeleteView(OccurrenceViewMixin, DeleteView): 331 | """View to delete an occurrence of an event.""" 332 | def delete(self, request, *args, **kwargs): 333 | self.object = self.get_object() 334 | decision = self.request.POST.get('decision') 335 | self.object.delete_period(decision) 336 | return HttpResponseRedirect(self.get_success_url()) 337 | 338 | def get_context_data(self, object): 339 | ctx = super(OccurrenceDeleteView, self).get_context_data() 340 | ctx.update({ 341 | 'decisions': OCCURRENCE_DECISIONS, 342 | 'object': self.object 343 | }) 344 | return ctx 345 | 346 | def get_success_url(self): 347 | return reverse('calendar_current_month') 348 | 349 | 350 | class OccurrenceDetailView(OccurrenceViewMixin, DetailView): 351 | """View to show information of an occurrence of an event.""" 352 | pass 353 | 354 | 355 | class OccurrenceUpdateView(OccurrenceViewMixin, UpdateView): 356 | """View to edit an occurrence of an event.""" 357 | pass 358 | 359 | 360 | class UpcomingEventsAjaxView(CategoryMixin, ListView): 361 | template_name = 'calendarium/partials/upcoming_events.html' 362 | context_object_name = 'occurrences' 363 | 364 | def dispatch(self, request, *args, **kwargs): 365 | if request.GET.get('category'): 366 | self.category = EventCategory.objects.get( 367 | slug=request.GET.get('category')) 368 | else: 369 | self.category = None 370 | if request.GET.get('count'): 371 | self.count = int(request.GET.get('count')) 372 | else: 373 | self.count = None 374 | return super(UpcomingEventsAjaxView, self).dispatch( 375 | request, *args, **kwargs) 376 | 377 | def get_context_data(self, **kwargs): 378 | ctx = super(UpcomingEventsAjaxView, self).get_context_data(**kwargs) 379 | ctx.update(self.get_category_context(**kwargs)) 380 | ctx.update({'show_excerpt': True, }) 381 | return ctx 382 | 383 | def get_queryset(self): 384 | qs_kwargs = { 385 | 'start': now(), 386 | 'end': now() + timedelta(365), 387 | } 388 | if self.category: 389 | qs_kwargs.update({'category': self.category, }) 390 | qs = Event.objects.get_occurrences(**qs_kwargs) 391 | if self.count: 392 | return qs[:self.count] 393 | return qs 394 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-calendarium.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-calendarium.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-calendarium" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-calendarium" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-calendarium.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-calendarium.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | # 4 | # django-calendarium documentation build configuration file, created by 5 | # sphinx-quickstart on Fri May 3 09:21:59 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys, os 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'django-calendarium' 45 | copyright = u'2013, Martin Brochhaus' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.2' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.2' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = [] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | # If true, keep warnings as "system message" paragraphs in the built documents. 91 | #keep_warnings = False 92 | 93 | 94 | # -- Options for HTML output --------------------------------------------------- 95 | 96 | # The theme to use for HTML and HTML Help pages. See the documentation for 97 | # a list of builtin themes. 98 | html_theme = 'default' 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | #html_theme_options = {} 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | #html_theme_path = [] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | #html_title = None 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | #html_short_title = None 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | #html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_domain_indices = True 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | #html_show_sourcelink = True 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | #html_show_sphinx = True 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | #html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | #html_use_opensearch = '' 166 | 167 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = None 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'django-calendariumdoc' 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | latex_elements = { 177 | # The paper size ('letterpaper' or 'a4paper'). 178 | #'papersize': 'letterpaper', 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #'pointsize': '10pt', 182 | 183 | # Additional stuff for the LaTeX preamble. 184 | #'preamble': '', 185 | } 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, author, documentclass [howto/manual]). 189 | latex_documents = [ 190 | ('index', 'django-calendarium.tex', u'django-calendarium Documentation', 191 | u'Martin Brochhaus', 'manual'), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | #latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | #latex_use_parts = False 201 | 202 | # If true, show page references after internal links. 203 | #latex_show_pagerefs = False 204 | 205 | # If true, show URL addresses after external links. 206 | #latex_show_urls = False 207 | 208 | # Documents to append as an appendix to all manuals. 209 | #latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | #latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [ 220 | ('index', 'django-calendarium', u'django-calendarium Documentation', 221 | [u'Martin Brochhaus'], 1) 222 | ] 223 | 224 | # If true, show URL addresses after external links. 225 | #man_show_urls = False 226 | 227 | 228 | # -- Options for Texinfo output ------------------------------------------------ 229 | 230 | # Grouping the document tree into Texinfo files. List of tuples 231 | # (source start file, target name, title, author, 232 | # dir menu entry, description, category) 233 | texinfo_documents = [ 234 | ('index', 'django-calendarium', u'django-calendarium Documentation', 235 | u'Martin Brochhaus', 'django-calendarium', 'One line description of project.', 236 | 'Miscellaneous'), 237 | ] 238 | 239 | # Documents to append as an appendix to all manuals. 240 | #texinfo_appendices = [] 241 | 242 | # If false, no module index is generated. 243 | #texinfo_domain_indices = True 244 | 245 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 246 | #texinfo_show_urls = 'footnote' 247 | 248 | # If true, do not generate a @detailmenu in the "Top" node's menu. 249 | #texinfo_no_detailmenu = False 250 | 251 | 252 | # Example configuration for intersphinx: refer to the Python standard library. 253 | intersphinx_mapping = {'http://docs.python.org/': None} 254 | -------------------------------------------------------------------------------- /docs/source/contribute.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | If you want to contribute to this project, please perform the following steps:: 5 | 6 | # Fork this repository 7 | # Clone your fork 8 | $ mkvirtualenv django-calendarium 9 | $ pip install -r requirements.txt 10 | $ pip install -r test_requirements.txt 11 | $ ./runtests.py 12 | # You should get no failing tests 13 | 14 | $ git checkout -b feature_branch master 15 | # Implement your feature and tests 16 | # Describe your change in the CHANGELOG.txt 17 | $ git add . && git commit 18 | $ git push -u origin HEAD 19 | # Send us a pull request for your feature branch 20 | 21 | Whenever you run the tests a coverage output will be generated in 22 | ``calendarium/tests/coverage/index.html``. When adding new features, please 23 | make sure that you keep the coverage at 100%. 24 | 25 | 26 | Updating the documentation 27 | -------------------------- 28 | 29 | In order to update the documentation, just edit the ``.rst`` files in 30 | ``docs/source``. If you would like to see your changes in the browser, run 31 | ``make html`` in ``docs``. 32 | 33 | Check out the `reStructuredText Primer `_ for 34 | more information on the concepts and syntax. 35 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. django-calendarium documentation master file, created by 2 | sphinx-quickstart on Fri May 3 09:21:59 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-calendarium's documentation! 7 | ============================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | installation 15 | usage 16 | contribute 17 | 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | 27 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | If you want to install the latest stable release from PyPi:: 5 | 6 | $ pip install django-calendarium 7 | 8 | If you feel adventurous and want to install the latest commit from GitHub:: 9 | 10 | $ pip install -e git+git://github.com/bitmazk/django-calendarium.git#egg=calendarium 11 | 12 | Add ``calendarium`` (and the ``filer`` dependencies) to your ``INSTALLED_APPS``:: 13 | 14 | INSTALLED_APPS = ( 15 | ..., 16 | 'filer', 17 | 'mptt', 18 | 'easy_thumbnails', 19 | 'calendarium', 20 | ) 21 | 22 | Add ``django.template.context_processors.request`` to your ``TEMPLATE_CONTEXT_PROCESSORS``:: 23 | 24 | TEMPLATE_CONTEXT_PROCESSORS = ( 25 | ..., 26 | 'django.template.context_processors.request', 27 | ) 28 | 29 | Add the urls to your main ``urls.py``:: 30 | 31 | urlpatterns = patterns('', 32 | ... 33 | url(r'^calendar/', include('calendarium.urls')), 34 | ) 35 | 36 | If you are using a Django version below 1.7, add the following setting: 37 | 38 | SOUTH_MIGRATION_MODULES = { 39 | 'calendarium': 'calendarium.south_migrations', 40 | } 41 | 42 | Run the migrations:: 43 | 44 | ./manage.py migrate 45 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | First think about the layout you want to integrate. In most cases you might 5 | want to add the calendar for the current month first. Reverse with:: 6 | 7 | {% url "calendar_current_month" %} 8 | 9 | Then use the different URLs to let the user navigate through months, weeks and 10 | days. The event management should be intuitive. Be sure to add ``Rule`` models, 11 | if you want to use occurrences. 12 | 13 | Template Tags 14 | ------------- 15 | 16 | When using the calendarium template tags in your template, include:: 17 | 18 | {% load calendarium_tags %} 19 | 20 | We provide a template tag to render a defined amount of upcoming occurrences:: 21 | 22 | {% render_upcoming_events %} 23 | 24 | The default amount is ``5``. You can add your own:: 25 | 26 | {% render_upcoming_events 1000 %} 27 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault( 7 | 'DJANGO_SETTINGS_MODULE', 'calendarium.tests.settings') 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Install these requirements if you wish to contribute to this project. 2 | 3 | # =========================================================================== 4 | # Packages essential to this app. Needed by anyone who wants to use this app. 5 | # =========================================================================== 6 | django 7 | python-dateutil 8 | django-filer 9 | django-libs 10 | Pillow 11 | pytz 12 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This script is used to run tests, create a coverage report and output the 4 | statistics at the end of the tox run. 5 | To run this script just execute ``tox`` 6 | """ 7 | import re 8 | 9 | from fabric.api import local, warn 10 | from fabric.colors import green, red 11 | 12 | 13 | if __name__ == '__main__': 14 | local('flake8 --ignore=E126 --ignore=W391 --statistics' 15 | ' --exclude=submodules,south_migrations,migrations,build .') 16 | local('coverage run --source="calendarium" manage.py test -v 2' 17 | ' --traceback --failfast --settings=calendarium.tests.settings' 18 | ' --pattern="*_tests.py"') 19 | local('coverage html -d coverage' 20 | ' --omit="*__init__*,*/settings/*,*/south_migrations/*,' 21 | '*/migrations/*,*/tests/*,*admin*"') 22 | total_line = local('grep -n pc_cov coverage/index.html', capture=True) 23 | percentage = float(re.findall(r'(\d+)%', total_line)[-1]) 24 | if percentage < 100: 25 | warn(red('Coverage is {0}%'.format(percentage))) 26 | else: 27 | print(green('Coverage is {0}%'.format(percentage))) 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | import calendarium 4 | try: 5 | import multiprocessing # NOQA 6 | except ImportError: 7 | pass 8 | 9 | 10 | def read(fname): 11 | try: 12 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 13 | except IOError: 14 | return '' 15 | 16 | 17 | setup( 18 | name="django-calendarium", 19 | version=calendarium.__version__, 20 | description=read('DESCRIPTION'), 21 | long_description=read('README.rst'), 22 | license='The MIT License', 23 | platforms=['OS Independent'], 24 | keywords='django, calendar, app, widget, events, schedule', 25 | author='Daniel Kaufhold', 26 | author_email='daniel.kaufhold@bitmazk.com', 27 | url="https://github.com/bitmazk/django-calendarium", 28 | packages=find_packages(), 29 | include_package_data=True, 30 | install_requires=[ 31 | 'django>=1.6', 32 | 'python-dateutil', 33 | 'django-filer', 34 | 'django-libs', 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | mixer 2 | pytz 3 | django-libs 4 | fabric3 5 | mock 6 | coverage 7 | django-coverage 8 | ipdb 9 | flake8 10 | tox 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27-django{18,19},py35-django19 3 | 4 | [testenv] 5 | usedevelop = True 6 | deps = 7 | django18: Django>=1.8,<1.9 8 | django19: Django>=1.9,<1.10 9 | -rtest_requirements.txt 10 | commands = python runtests.py 11 | --------------------------------------------------------------------------------