diff --git a/app.py b/app.py index e3d16de..c0f8f0b 100644 --- a/app.py +++ b/app.py @@ -11,6 +11,7 @@ from flask_principal import Permission from datetime import datetime import pytz from urllib.parse import quote_plus +from dateutil.rrule import rrulestr, rruleset, rrule # Create app app = Flask(__name__) @@ -44,6 +45,10 @@ from models import EventCategory, Image, EventSuggestion, EventSuggestionDate, O user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role) security = Security(app, user_datastore) +berlin_tz = pytz.timezone('Europe/Berlin') +now = datetime.now(tz=berlin_tz) +today = datetime(now.year, now.month, now.day, tzinfo=now.tzinfo) + @babel.localeselector def get_locale(): return request.accept_languages.best_match(app.config['LANGUAGES']) @@ -207,7 +212,7 @@ def upsert_organization(org_name, street = None, postalCode = None, city = None, return result def create_berlin_date(year, month, day, hour, minute = 0): - return pytz.timezone('Europe/Berlin').localize(datetime(year, month, day, hour=hour, minute=minute)) + return berlin_tz.localize(datetime(year, month, day, hour=hour, minute=minute)) def upsert_actor_for_admin_unit(admin_unit_id): result = Actor.query.filter_by(admin_unit_id = admin_unit_id).first() @@ -296,11 +301,16 @@ def upsert_event_category(category_name): return result -def upsert_event(event_name, host, location_name, start, description, link = None, verified = False, admin_unit = None, ticket_link=None, photo_res=None, category=None): +def upsert_event(event_name, host, location_name, start, description, link = None, verified = False, admin_unit = None, ticket_link=None, photo_res=None, category=None, recurrence_rule=None): if admin_unit is None: admin_unit = get_admin_unit('Stadt Goslar') place = upsert_place(location_name) + if category is not None: + category_object = upsert_event_category(category) + else: + category_object = upsert_event_category('Other') + result = Event.query.filter_by(name = event_name).first() if result is None: result = Event() @@ -314,19 +324,25 @@ def upsert_event(event_name, host, location_name, start, description, link = Non result.host = host result.place = place result.ticket_link = ticket_link + result.category = category_object - eventDate = EventDate(event_id = result.id, start=start) result.dates = [] - result.dates.append(eventDate) + + if recurrence_rule is not None: + result.recurrence_rule = recurrence_rule + start_wo_tz = start.replace(tzinfo=None) + rule_set = rrulestr(recurrence_rule, forceset=True, dtstart=start_wo_tz) + for rule_date in list(rule_set): + rule_data_w_tz = berlin_tz.localize(rule_date) + eventDate = EventDate(event_id = result.id, start=rule_data_w_tz) + result.dates.append(eventDate) + else: + eventDate = EventDate(event_id = result.id, start=start) + result.dates.append(eventDate) if photo_res is not None: result.photo = upsert_image_with_res(result.photo, photo_res) - if category is not None: - result.category = upsert_event_category(category) - else: - result.category = upsert_event_category('Other') - return result def get_event_hosts(): @@ -625,6 +641,7 @@ def create_user(): upsert_event_category('Fitness') upsert_event_category('Sports') upsert_event_category('Other') + db.session.commit() # Admin units goslar = upsert_admin_unit('Stadt Goslar') @@ -643,7 +660,7 @@ def create_user(): # Organizations admin_unit_org_event_verifier_role = upsert_admin_unit_org_role('event_verifier', ['event:verify', "event:create"]) - gmg = upsert_organization("GOSLAR marketing gmbh") + gmg = upsert_organization("GOSLAR marketing gmbh", "Markt 7", "38640", "Goslar", url='https://www.goslar.de/kontakt', logo_res="gmg.jpeg") gz = upsert_organization("Goslarsche Zeitung") celtic_inn = upsert_organization("Celtic Inn") kloster_woelteringerode = upsert_organization("Kloster Wöltingerode") @@ -748,13 +765,13 @@ def create_user(): upsert_place('Mehrzweckhalle Hahndorf') upsert_place('Sportplatz Hahndorf') upsert_place('St. Kilian Hahndorf') - upsert_place("Tourist-Information Goslar", 'Markt 7', '38644', 'Goslar', 51.906172, 10.429346) + upsert_place("Tourist-Information Goslar", 'Markt 7', '38644', 'Goslar', 51.906172, 10.429346, 'http://www.goslar.de/kontakt', "Zentral am Marktplatz gelegen, ist die Tourist-Information die erste Anlaufstelle für Goslar-Besucher. 14 motivierte und serviceorientierte Mitarbeiter sorgen dafür, dass sich der Gast rundherum wohl fühlt. Die ständige Qualitätsverbesserung und der flexible Umgang mit den Kundenwünschen sind Teil unserer Leitsätze.", 'touristinfo.jpeg') upsert_place("Nagelkopf am Rathaus Goslar", 'Marktkirchhof 3', '38640', 'Goslar', 51.9055939, 10.4263286) upsert_place("Kloster Wöltingerode", 'Wöltingerode 3', '38690', 'Goslar', 51.9591156, 10.5371815) upsert_place("Marktplatz Goslar", 'Markt 6', '38640', 'Goslar', 51.9063601, 10.4249433) upsert_place("Burg Vienenburg", 'Burgweg 2', '38690', 'Goslar', 51.9476558, 10.5617368) upsert_place("Kurhaus Bad Harzburg", 'Kurhausstraße 11', '38667', 'Bad Harzburg', 51.8758165, 10.5593392) - upsert_place("Goslarsche Höfe", 'Okerstraße 32', '38640', 'Goslar', 51.911571, 10.4391331, 'https://www.goslarsche-hoefe.de/', 'Dir Rosserei', photo_res="schlosserei.jpeg") + upsert_place("Goslarsche Höfe", 'Okerstraße 32', '38640', 'Goslar', 51.911571, 10.4391331, 'https://www.goslarsche-hoefe.de/') upsert_place("Schlosserei im Rammelsberg", 'Bergtal 19', '38640', 'Goslar', 51.890527, 10.418880, 'http://www.rammelsberg.de/', 'Die "Schlosserei" ist erprobter Veranstaltungsort und bietet Platz für ca. 700 Besucher. Das Ambiente ist technisch gut ausgestattet und flexibel genug, für jeden Künstler individuell wandelbar zu sein. Dabei lebt nicht nur der Veranstaltungsraum, es wirkt der gesamte Komplex des Rammelsberges und macht den Besuch zu einem unvergeßlichen Erlebnis.', photo_res="schlosserei.jpeg") # Org or admins @@ -804,7 +821,6 @@ def create_user(): add_role_to_org_member(grzno_miners_rock_member, org_member_event_creator_role) # Events - berlin = pytz.timezone('Europe/Berlin') upsert_event("Vienenburger Seefest", goslar_ooa, "Vienenburger See", @@ -814,16 +830,20 @@ def create_user(): True) upsert_event("Tausend Schritte durch die Altstadt", - goslar_ooa, + gmg_ooa, "Tourist-Information Goslar", - create_berlin_date(2020, 9, 1, 10, 0), - 'Tausend Schritte durch die Altstadt Erleben Sie einen geführten Stadtrundgang durch den historischen Stadtkern. Lassen Sie sich von Fachwerkromantik und kaiserlichen Bauten inmitten der UNESCO-Welterbestätte verzaubern. ganzjährig (außer 01.01.) täglich 10:00 Uhr Treffpunkt: Tourist-Information am Marktplatz (Dauer ca. 2 Std.) Erwachsene 8,00 Euro Inhaber Gastkarte Goslar/Kurkarte Hahnenklee 7,00 Euro Schüler/Studenten 6,00 Euro') + create_berlin_date(2020, 1, 2, 10, 0), + 'Tausend Schritte durch die Altstadt Erleben Sie einen geführten Stadtrundgang durch den historischen Stadtkern. Lassen Sie sich von Fachwerkromantik und kaiserlichen Bauten inmitten der UNESCO-Welterbestätte verzaubern. ganzjährig (außer 01.01.) täglich 10:00 Uhr Treffpunkt: Tourist-Information am Marktplatz (Dauer ca. 2 Std.) Erwachsene 8,00 Euro Inhaber Gastkarte Goslar/Kurkarte Hahnenklee 7,00 Euro Schüler/Studenten 6,00 Euro', + photo_res="tausend.jpeg", + recurrence_rule="FREQ=DAILY;UNTIL=20201231T235959") upsert_event("Spaziergang am Nachmittag", - goslar_ooa, + gmg_ooa, "Tourist-Information Goslar", - create_berlin_date(2020, 9, 1, 13, 30), - 'Spaziergang am Nachmittag Begeben Sie sich auf einen geführten Rundgang durch die historische Altstadt. Entdecken Sie malerische Fachwerkgassen und imposante Bauwerke bei einem Streifzug durch das UNESCO-Weltkulturerbe. April – Oktober und 25.11. – 30.12. Montag – Samstag 13:30 Uhr Treffpunkt: Tourist-Information am Marktplatz (Dauer ca. 1,5 Std.) Erwachsene 7,00 Euro Inhaber Gastkarte Goslar/Kurkarte Hahnenklee 6,00 Euro Schüler/Studenten 5,00 Euro') + create_berlin_date(2020, 4, 1, 13, 30), + 'Spaziergang am Nachmittag Begeben Sie sich auf einen geführten Rundgang durch die historische Altstadt. Entdecken Sie malerische Fachwerkgassen und imposante Bauwerke bei einem Streifzug durch das UNESCO-Weltkulturerbe. April – Oktober und 25.11. – 30.12. Montag – Samstag 13:30 Uhr Treffpunkt: Tourist-Information am Marktplatz (Dauer ca. 1,5 Std.) Erwachsene 7,00 Euro Inhaber Gastkarte Goslar/Kurkarte Hahnenklee 6,00 Euro Schüler/Studenten 5,00 Euro', + photo_res="nachmittag.jpeg", + recurrence_rule="""RRULE:FREQ=WEEKLY;UNTIL=20201031T235959;BYDAY=MO,TU,WE,TH,FR,SA""") upsert_event("Ein Blick hinter die Kulissen - Rathausbaustelle", goslar_ooa, @@ -946,9 +966,9 @@ def place(place_id): @app.route("/events") def events(): - events = Event.query.all() + dates = EventDate.query.filter(EventDate.start >= today).order_by(EventDate.start).all() return render_template('events.html', - events=events, + dates=dates, user_can_create_event=can_create_event(), user_can_list_event_suggestion=can_list_event_suggestion()) diff --git a/migrations/versions/4c52ae230b29_.py b/migrations/versions/bbad7e33a780_.py similarity index 98% rename from migrations/versions/4c52ae230b29_.py rename to migrations/versions/bbad7e33a780_.py index 54e3998..b31ed3d 100644 --- a/migrations/versions/4c52ae230b29_.py +++ b/migrations/versions/bbad7e33a780_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 4c52ae230b29 +Revision ID: bbad7e33a780 Revises: -Create Date: 2020-06-23 14:55:52.652970 +Create Date: 2020-06-24 21:17:25.548159 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '4c52ae230b29' +revision = 'bbad7e33a780' down_revision = None branch_labels = None depends_on = None @@ -241,7 +241,8 @@ def upgrade(): sa.Column('ticket_link', sa.String(length=255), nullable=True), sa.Column('verified', sa.Boolean(), nullable=True), sa.Column('photo_id', sa.Integer(), nullable=True), - sa.Column('category_id', sa.Integer(), nullable=True), + sa.Column('category_id', sa.Integer(), nullable=False), + sa.Column('recurrence_rule', sa.UnicodeText(), nullable=True), sa.Column('created_by_id', sa.Integer(), nullable=True), sa.ForeignKeyConstraint(['admin_unit_id'], ['adminunit.id'], ), sa.ForeignKeyConstraint(['category_id'], ['eventcategory.id'], ), diff --git a/models.py b/models.py index 31fcdd5..d101fa3 100644 --- a/models.py +++ b/models.py @@ -244,6 +244,7 @@ class Event(db.Model, TrackableMixin): category_id = db.Column(db.Integer, db.ForeignKey('eventcategory.id'), nullable=False) category = relationship('EventCategory', uselist=False) + recurrence_rule = Column(UnicodeText()) dates = relationship('EventDate', backref=backref('event', lazy=False), cascade="all, delete-orphan") # wiederkehrende Dates sind zeitlich eingeschränkt # beim event müsste man dann auch nochmal start_time (nullable=False) und end_time machen. diff --git a/static/img/gmg.jpeg b/static/img/gmg.jpeg new file mode 100644 index 0000000..3f5ac37 Binary files /dev/null and b/static/img/gmg.jpeg differ diff --git a/static/img/nachmittag.jpeg b/static/img/nachmittag.jpeg new file mode 100644 index 0000000..cc16015 Binary files /dev/null and b/static/img/nachmittag.jpeg differ diff --git a/static/img/tausend.jpeg b/static/img/tausend.jpeg new file mode 100644 index 0000000..44dec03 Binary files /dev/null and b/static/img/tausend.jpeg differ diff --git a/static/img/touristinfo.jpeg b/static/img/touristinfo.jpeg new file mode 100644 index 0000000..ce9d6f4 Binary files /dev/null and b/static/img/touristinfo.jpeg differ diff --git a/static/jquery.recurrenceinput.css b/static/jquery.recurrenceinput.css new file mode 100644 index 0000000..9d77800 --- /dev/null +++ b/static/jquery.recurrenceinput.css @@ -0,0 +1,226 @@ +div.riform { + padding: 1em; + background-color: white; + box-shadow: 0 0 3em 0.5em #666; + line-height: 2; + -moz-box-shadow: 0 0 3em 0.5em #666; + -webkit-box-shadow: 0 0 3em #666; +} + +div.riform h1 { + color: #888888; + border-bottom: 1px solid #DDDDDD; + font-size: 20px; + line-height: 1; + margin: 0; + padding-bottom: 5px; + padding-left: 5px; +} + +div.riform form { + margin-bottom: 0; +} + +div.riform .rifield { + clear: both; +} + +div.riform .rifield .field { + float:left; + clear: none; +} + +div.riform .label { + display: block; + float: left; + font-weight: bold; + margin-right: 10px; + text-align: right; + width: 130px; +} + +div.riform #rirtemplate { + margin-top: 6px; +} + +div#riformfields { + min-height: 11em; + min-width: 25em; +} + +div.riform #rirangeoptions input, +div.riform #rimonthlyoptions input, +div.riform #riyearlyoptions input { + margin: 0; +} + +div.riform #riweeklyweekdays .riweeklyweekday input { + display:block; + margin: 8px auto 0; +} +div.riform #riweeklyweekdays .riweeklyweekday label { + display:block; +} + +div.riform #riweeklyweekdays .riweeklyweekday { + margin-right: 15px; + float: left; +} + +div.riform input.ricancelbutton { + // pb_close.png + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6QAAABHVBMVEUAAAAAAAADAwMEBAQFBQUGBgYJCQkKCgoPDw8RERETExMWFhYbGxscHBwfHx8iIiIlJSUoKCgsLCwyMjI1NTU4ODg7OztDQ0NGRkZKSkpLS0tNTU1XV1ddXV1gYGBjY2NkZGRmZmZoaGhsbGxvb291dXV7e3t+fn6BgYGCgoKFhYWLi4uMjIyNjY2Ojo6QkJCTk5OVlZWWlpaXl5eZmZmdnZ2fn5+qqqqurq6vr6+ysrKzs7O5ubnCwsLFxcXHx8fJycnLy8vMzMzR0dHV1dXY2Nja2trc3Nzd3d3f39/g4ODh4eHk5OTl5eXo6Ojr6+vs7Ozt7e3u7u7x8fHy8vLz8/P19fX29vb39/f4+Pj6+vr7+/v8/Pz9/f3////kwcJJAAAAAXRSTlMAQObYZgAAAThJREFUKBXNwWdb2gAYhtH3CVbQFisVlOLWVlHcdeBedXSQqoiR4f3/f4YJF8Gg9psfPMfezL8avvqNvaaCz/2Nr2wvPFBbz8qXXrynZs/AXp9aUidgHRpsOmpz5sEibthzFLVGxZ5wl5Q0MCjfx/G49KGEtf1iRdIXyEufS6xLmuTKQh4ZSX0e5PtLUJDU28BC3CqQ8+AWio58p1iIczXlqkDRUWCHH9bCTzWlroA5NW2zai1cK5ByoQF5BY74bi01kpISLhSHPZiW5JSx0CXfJGWg6CjnsSUpi2uhJdy4pMlpR1I6n5S0z661lSiowxgVezIBY4oYrLNqEWswq7bRKgcW1bMCZyNxBbKHcCDrkJj6C9XTjY3jMlwvyJ6Ja/mCpj+bXbKXupUYmpn5+kmy/4jFFLP34hGuw0GxTwkuWgAAAABJRU5ErkJggg==); + background-color: transparent; + font-size: 0; /* For IE8 */ + color: transparent; + border: none; + position: absolute; + left: -14px; + top: -14px; + cursor: pointer; + height: 30px; + width: 30px; +} + +div.rioccurrencesactions .riaddoccurrence #adddate { + width: 75%; +} + +div.rioccurrencesactions .rioccurancesheader { + border-bottom: 1px solid #DDDDDD; + line-height: 1.5; + clear: both; + margin-top: 30px; +} + +div.rioccurrencesactions .rioccurancesheader h2 { + color: #888888; + display: inline; + font-size: 18px; + font-weight: bold; + margin: 0px 0px 5px 5px; +} + + +div.rioccurrences div.batching { + font-size: 70%; + text-align: center; +} + +div.rioccurrences span.current { + font-weight: bold; +} + +div.riform span.action a { + height: 19px; + width: 19px; + overflow: hidden; + float: right; + text-indent: 9999px; +} + +div.rioccurrences .occurrence { + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; +} + +div.rioccurrences .occurrence:hover { + border-top: 1px solid #DDDDDD; + border-bottom: 1px solid #DDDDDD; +} + +div.rioccurrences .occurrence.start span.rlabel, +div.rioccurrences .occurrence.rdate span.rlabel { + color: #9CBA9B; + margin: 0 5px; + font-size: 70%; + font-weight: bold; +} + +div.rioccurrences .occurrence.exdate { + opacity:0.4; + filter:alpha(opacity=40); +} + +div.ridisplay .occurrence.exdate { + display: none; +} + +div.ridisplay label.ridisplay { + font-weight: 300; +} + +div.ridisplay .rimain a { + margin-right: 0.5em; +} + +div.rioccurrences .occurrence.rdate { + background: #FFFFE0; +} + +div.rioccurrences div.occurrence { + margin-left: 5px; +} + +div.rioccurrences a.rrule, +div.rioccurrences a.rdate, +div.rioccurrences a.exdate { + color: transparent; + margin-top: 6px; + margin-right: 5px; +} + + +div.rioccurrences a.rrule, +div.rioccurrences a.rdate { + // delete.png + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAMAAABFjsb+AAAAdVBMVEUAAAAAAAACAgIHBwcICAgODg4PDw8QEBASEhIXFxcbGxsdHR0fHx8gICAnJyc2NjY3Nzc4ODg5OTk+Pj5ERERISEhJSUlSUlJYWFhqampvb2/Hx8fPz8/R0dHn5+fs7Ozv7+/x8fHy8vL39/f9/f3+/v7///8jaUCMAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAAAOkAAADpAVSSFEsAAAB9SURBVBjTY2BQQwcMDGqyYuxsCMAuJqvGoCbCiApEgGIsECaTEBOEwQIUYwbSbAKCUmqSggKsQDYzVEwGarw0kpiwKlhIhR9JjEVNXFlMWUKNEUWMU4FZgYfGYrxKHEp8KGJM8opqQCgHEwP7l1sUBLhg/sUWLljCD0s4AwBmjBYPljOv7QAAAABJRU5ErkJggg==); +} + +div.rioccurrences a.exdate { + // undelete.png + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAMAAABFjsb+AAAAnFBMVEUCodIAAAABAQEVAAAHBwcICAgKCgoPDw8QEBBTAABVAABZAABaAAAXFxdcBwdhCgodHR2TAAAhISFlDw9nDw8vLy8wMDAxMTH4AAD5AQH6AgL+Bgb/BweDKipBQUFERERJSUlQUFBXV1daWlpmZmb0nZ31nZ36o6P/pqb/qanNzc3R0dHp6ens7Ozt7e3v7+/y8vL39/f8/Pz////TudgfAAAAAXRSTlMAQObYZgAAAKFJREFUGFdt0FsXQkAUhuHZIYfIITTVFJVEiNr//781xoUJ79Ws52KvNR8hOI0QrFJDHzPSCgkmoIBcwk0D3+LP1X41mMZNBZNuozjDLI7W3FRhYJ/K4fxrNDgzgd+LZFAcn7fPHUE2DJnbOhNzcprPrHVZODP9cShm1u7Y5s+8zum8ktqS1Q2+saktagrTeguufQFYvvhvAnKK2GVhv4WdfzaPHhGyo11gAAAAAElFTkSuQmCC); +} + +div.rioccurrencesactions a.rirefreshbutton { + // refresh.png + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAMAAABFjsb+AAAA1VBMVEWbLGwAAAABAQECAgIFBQUGBgYHBwcICAgJCQkKCgoWFhYZGRkaGhoeHh4fHx8gICAkJCQmJiYoKCgrKyssLCwxMTE3Nzc4ODg5OTk+Pj5DQ0NHR0dISEhRUVFUVFRWVlZXV1ddXV1fX19gYGBkZGRoaGhwcHBxcXF6enp/f3+BgYGDg4OEhISJiYmNjY2VlZWampqnp6eoqKirq6uurq62trbCwsLDw8PFxcXIyMjNzc3V1dXW1tbZ2dnb29vv7+/19fX39/f4+Pj6+vr7+/v8/Pz///9okOBsAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAAAOkAAADpAVSSFEsAAADYSURBVBjTZdBrk8EwFAbghEprFaulxN2y2HVZ6n5tWX3//0+ScIwZ3g/JmWcmOTlhDK9hDBtpCiEqqYTQMeUGDJLrtBcxfo9UJnSRnKCfbyV1KchKRyDq4Fh6mhchxF8hQOQ9zDpg1V0a3F7hYJE1EdjlD3XMDtAkG8Gnnj5GZDMMyYaYkQ0wJ5tjQFbDyVFgec4JNTJji73LY/7vHlvj8T43xLn4gwtC9zlHbo2vf2CdozlMfXlcfvYwjuvSVFalnnz6fduqynaNbEYn7dTVmm3s9Ke+/fMVeXMr/BRXPaEAAAAASUVORK5CYII=); + color: transparent; + margin-top: 4px; + margin-right: 5px; +} + +div#messagearea, +div.errorarea { + display: none; + background-color: red; + color: white; + font-weight: bold; + padding: 2px 10px; +} + +div#calroot { + z-index:10000; +} + +div.ributtons .risavebutton { + display: block; + margin: 30px auto 0; +} + +// jQueryTools overrides +#calnext, +#calprev { + background-image: url(data:image/gif;base64,R0lGODlhDgAOAOZsAPb29vv7+/f39/Ly8vr6+vn5+QCFzfz8/PDw8PPz8wCEzQODzACHzwCFzgCBzCKb1fL7/fHx8aXZ7+r4/F295LXg8pTR6wKCzA2L0J3V7ZnW7T234gaS05bV7v3+/ofN65Tb8JfX7/n+/pff8uv6/YXN60C748/s9wCIz9ry+gKHzgOIzs7r9gF+yx+Z1QKEzSKg2AGP0mG54uf2++b4/AyP0QGCzGG844bN62m95Gm/5Fm95AeQ0vf8/bzl9ACAyxma1rbi9DKm2////weGzpfW7Mvp9QyO0J/Y7qrc8AiJz9jy+mK949Tv+QCDzHLA5geIzgCDzcfr9gqM0Mzv+ACQ0g2Q0sLn9XbM6y+p3L/s96LY706w3sbo9PX19ZXT7PD7/Tis3gyQ0g+R0v7+/gCGzgCHzvj4+ACCzPT09O/v7wCJz+7u7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAGwALAAAAAAOAA4AAAe0gGyCamlnBAQCCYKLbAMHTVgbJh00BWqLA2QlMQpoCxdAQQGXagc4CmZlRxIWC1ZXZ2xpVBwGZTVIZBlKDllgCAUfaAxTGkNfUAxraydpATloK0VkW0Rmyw1JAAFPDio6EEYuUcsGFQBnIQZmNkwiLA9lZlUpCQMkME4oLzceXRg/KPS4VMDHGDRl0Mjg0kLIDC+DCEgJw2NNAzE7JghgpEYAGS0gRiw5kIbRogjozqRBwCgQADs=); +} +#calnext { + background-image: url(data:image/gif;base64,R0lGODlhDgAOAOZsAPb29vv7+/f39/Ly8vr6+vn5+QCFzfz8/PDw8PPz8wCEzQODzACHzwCFzgCBzCKb1fL7/fHx8aXZ7+r4/F295LXg8pTR6wKCzA2L0J3V7ZnW7T234gaS05bV7v3+/ofN65Tb8JfX7/n+/pff8uv6/YXN60C748/s9wCIz9ry+gKHzgOIzs7r9gF+yx+Z1QKEzSKg2AGP0mG54uf2++b4/AyP0QGCzGG844bN62m95Gm/5Fm95AeQ0vf8/bzl9ACAyxma1rbi9DKm2////weGzpfW7Mvp9QyO0J/Y7qrc8AiJz9jy+mK949Tv+QCDzHLA5geIzgCDzcfr9gqM0Mzv+ACQ0g2Q0sLn9XbM6y+p3L/s96LY706w3sbo9PX19ZXT7PD7/Tis3gyQ0g+R0v7+/gCGzgCHzvj4+ACCzPT09O/v7wCJz+7u7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAGwALAAAAAAOAA4AAAe2gGyCbAhpZ2cAEYOLaQdLIyBaZAJqiwITO2INazxhUgSVbF4zQi1cMmhlaGM+BWxqPRQ/GF0eNy8oTjAkAwkpVWZlDywiTDZmBiGIFQZra1EuRhA6Kg5PAQBJm2tmRFtkRStoOQFpJ84MUF9DGlMMaB8FCGBZDkoZZEg1ZQYcVGlszlyxssCChCNlzCjAcaCSmgBBgFxYgEZBjBJkBgxSU4BGBxMbsDQ5oHERmwQCCBA4kyYUm0AAOw==); +} +div.overlaybg div.close, +div.overlay div.close { + // pb_close.png + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6QAAABHVBMVEUAAAAAAAADAwMEBAQFBQUGBgYJCQkKCgoPDw8RERETExMWFhYbGxscHBwfHx8iIiIlJSUoKCgsLCwyMjI1NTU4ODg7OztDQ0NGRkZKSkpLS0tNTU1XV1ddXV1gYGBjY2NkZGRmZmZoaGhsbGxvb291dXV7e3t+fn6BgYGCgoKFhYWLi4uMjIyNjY2Ojo6QkJCTk5OVlZWWlpaXl5eZmZmdnZ2fn5+qqqqurq6vr6+ysrKzs7O5ubnCwsLFxcXHx8fJycnLy8vMzMzR0dHV1dXY2Nja2trc3Nzd3d3f39/g4ODh4eHk5OTl5eXo6Ojr6+vs7Ozt7e3u7u7x8fHy8vLz8/P19fX29vb39/f4+Pj6+vr7+/v8/Pz9/f3////kwcJJAAAAAXRSTlMAQObYZgAAAThJREFUKBXNwWdb2gAYhtH3CVbQFisVlOLWVlHcdeBedXSQqoiR4f3/f4YJF8Gg9psfPMfezL8avvqNvaaCz/2Nr2wvPFBbz8qXXrynZs/AXp9aUidgHRpsOmpz5sEibthzFLVGxZ5wl5Q0MCjfx/G49KGEtf1iRdIXyEufS6xLmuTKQh4ZSX0e5PtLUJDU28BC3CqQ8+AWio58p1iIczXlqkDRUWCHH9bCTzWlroA5NW2zai1cK5ByoQF5BY74bi01kpISLhSHPZiW5JSx0CXfJGWg6CjnsSUpi2uhJdy4pMlpR1I6n5S0z661lSiowxgVezIBY4oYrLNqEWswq7bRKgcW1bMCZyNxBbKHcCDrkJj6C9XTjY3jMlwvyJ6Ja/mCpj+bXbKXupUYmpn5+kmy/4jFFLP34hGuw0GxTwkuWgAAAABJRU5ErkJggg==); +} +#calroot { + width: auto; +} diff --git a/static/jquery.recurrenceinput.js b/static/jquery.recurrenceinput.js new file mode 100644 index 0000000..192491d --- /dev/null +++ b/static/jquery.recurrenceinput.js @@ -0,0 +1,1740 @@ +/*jslint regexp: false, continue: true, indent: 4 */ +/*global $, alert, jQuery */ +"use strict"; + +(function ($) { + $.tools = $.tools || {version: '@VERSION'}; + + var tool; + var LABELS = {}; + + tool = $.tools.recurrenceinput = { + conf: { + lang: 'en', + readOnly: false, + firstDay: 0, + + // "REMOTE" FIELD + startField: null, + startFieldYear: null, + startFieldMonth: null, + startFieldDay: null, + ajaxURL: null, + ajaxContentType: 'application/json; charset=utf8', + ributtonExtraClass: '', + + // INPUT CONFIGURATION + hasRepeatForeverButton: true, + + // FORM OVERLAY + formOverlay: { + speed: 'fast', + fixed: false + }, + + // JQUERY TEMPLATE NAMES + template: { + form: '#jquery-recurrenceinput-form-tmpl', + display: '#jquery-recurrenceinput-display-tmpl' + }, + + // RECURRENCE TEMPLATES + rtemplate: { + daily: { + rrule: 'FREQ=DAILY', + fields: [ + 'ridailyinterval', + 'rirangeoptions' + ] + }, + mondayfriday: { + rrule: 'FREQ=WEEKLY;BYDAY=MO,FR', + fields: [ + 'rirangeoptions' + ] + }, + weekdays: { + rrule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', + fields: [ + 'rirangeoptions' + ] + }, + weekly: { + rrule: 'FREQ=WEEKLY', + fields: [ + 'riweeklyinterval', + 'riweeklyweekdays', + 'rirangeoptions' + ] + }, + monthly: { + rrule: 'FREQ=MONTHLY', + fields: [ + 'rimonthlyinterval', + 'rimonthlyoptions', + 'rirangeoptions' + ] + }, + yearly: { + rrule: 'FREQ=YEARLY', + fields: [ + 'riyearlyinterval', + 'riyearlyoptions', + 'rirangeoptions' + ] + } + } + }, + + localize: function (language, labels) { + LABELS[language] = labels; + }, + + setTemplates: function (templates, titles) { + var lang, template; + tool.conf.rtemplate = templates; + for (lang in titles) { + if (titles.hasOwnProperty(lang)) { + for (template in titles[lang]) { + if (titles[lang].hasOwnProperty(template)) { + LABELS[lang].rtemplate[template] = titles[lang][template]; + } + } + } + } + } + + }; + + tool.localize("en", { + displayUnactivate: 'Does not repeat', + displayActivate: 'Repeats every', + add_rules: 'Add', + edit_rules: 'Edit', + delete_rules: 'Delete', + add: 'Add', + refresh: 'Refresh', + + title: 'Repeat', + preview: 'Selected dates', + addDate: 'Add date', + + recurrenceType: 'Repeats:', + + dailyInterval1: 'Repeat every:', + dailyInterval2: 'days', + + weeklyInterval1: 'Repeat every:', + weeklyInterval2: 'week(s)', + weeklyWeekdays: 'Repeat on:', + weeklyWeekdaysHuman: 'on:', + + monthlyInterval1: 'Repeat every:', + monthlyInterval2: 'month(s)', + monthlyDayOfMonth1: 'Day', + monthlyDayOfMonth1Human: 'on day', + monthlyDayOfMonth2: 'of the month', + monthlyDayOfMonth3: 'month(s)', + monthlyWeekdayOfMonth1: 'The', + monthlyWeekdayOfMonth1Human: 'on the', + monthlyWeekdayOfMonth2: '', + monthlyWeekdayOfMonth3: 'of the month', + monthlyRepeatOn: 'Repeat on:', + + yearlyInterval1: 'Repeat every:', + yearlyInterval2: 'year(s)', + yearlyDayOfMonth1: 'Every', + yearlyDayOfMonth1Human: 'on', + yearlyDayOfMonth2: '', + yearlyDayOfMonth3: '', + yearlyWeekdayOfMonth1: 'The', + yearlyWeekdayOfMonth1Human: 'on the', + yearlyWeekdayOfMonth2: '', + yearlyWeekdayOfMonth3: 'of', + yearlyWeekdayOfMonth4: '', + yearlyRepeatOn: 'Repeat on:', + + range: 'End recurrence:', + rangeNoEnd: 'Never', + rangeByOccurrences1: 'After', + rangeByOccurrences1Human: 'ends after', + rangeByOccurrences2: 'occurrence(s)', + rangeByEndDate: 'On', + rangeByEndDateHuman: 'ends on', + + including: ', and also', + except: ', except for', + + cancel: 'Cancel', + save: 'Save', + + recurrenceStart: 'Start of the recurrence', + additionalDate: 'Additional date', + include: 'Include', + exclude: 'Exclude', + remove: 'Remove', + + orderIndexes: ['first', 'second', 'third', 'fourth', 'last'], + months: [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December'], + shortMonths: [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + weekdays: [ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday'], + shortWeekdays: [ + 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + + longDateFormat: 'mmmm dd, yyyy', + shortDateFormat: 'mm/dd/yyyy', + + unsupportedFeatures: 'Warning: This event uses recurrence features not ' + + 'supported by this widget. Saving the recurrence ' + + 'may change the recurrence in unintended ways:', + noTemplateMatch: 'No matching recurrence template', + multipleDayOfMonth: 'This widget does not support multiple days in monthly or yearly recurrence', + bysetpos: 'BYSETPOS is not supported', + noRule: 'No RRULE in RRULE data', + noRepeatEvery: 'Error: The "Repeat every"-field must be between 1 and 1000', + noEndDate: 'Error: End date is not set. Please set a correct value', + noRepeatOn: 'Error: "Repeat on"-value must be selected', + pastEndDate: 'Error: End date cannot be before start date', + noEndAfterNOccurrences: 'Error: The "After N occurrences"-field must be between 1 and 1000', + alreadyAdded: 'This date was already added', + + rtemplate: { + daily: 'Daily', + mondayfriday: 'Monday and Friday', + weekdays: 'Weekday', + weekly: 'Weekly', + monthly: 'Monthly', + yearly: 'Yearly' + } + }); + + + var OCCURRENCETMPL = ['
', + '{{each occurrences}}', + '
', + '', + '${occurrences[$index].formattedDate}', + '{{if occurrences[$index].type === "start"}}', + '${i18n.recurrenceStart}', + '{{/if}}', + '{{if occurrences[$index].type === "rdate"}}', + '${i18n.additionalDate}', + '{{/if}}', + '', + '{{if !readOnly}}', + '', + '{{if occurrences[$index].type === "rrule"}}', + '', + '${i18n.exclude}', + '', + '{{/if}}', + '{{if occurrences[$index].type === "rdate"}}', + '', + '${i18n.remove}', + '', + '{{/if}}', + '{{if occurrences[$index].type === "exdate"}}', + '', + '${i18n.include}', + '', + '{{/if}}', + '', + '{{/if}}', + '
', + '{{/each}}', + '
', + '{{each batch.batches}}', + '{{if $index === batch.currentBatch}}{{/if}}', + '[${batch.batches[$index][0]} - ${batch.batches[$index][1]}]', + '{{if $index === batch.currentBatch}}{{/if}}', + '{{/each}}', + '
'].join('\n'); + + $.template('occurrenceTmpl', OCCURRENCETMPL); + + var DISPLAYTMPL = ['
', + '
', + '{{if !readOnly}}', + '', + '', + '{{/if}}', + '', + '
', + ''].join('\n'); + + $.template('displayTmpl', DISPLAYTMPL); + + var FORMTMPL = ['
', + '
', + '

${i18n.title}

', + '', + '
', + '', + '', + '
', + '
', + '
', + '', + '
', + '', + '${i18n.dailyInterval2}', + '
', + '
', + '
', + '', + '
', + '', + '${i18n.weeklyInterval2}', + '
', + '
', + '
', + '', + '
', + '{{each orderedWeekdays}}', + '
', + '', + '', + '
', + '{{/each}}', + '
', + '
', + '
', + '', + '
', + '', + '${i18n.monthlyInterval2}', + '
', + '
', + '
', + '', + '
', + '
', + '', + '', + '', + '${i18n.monthlyDayOfMonth2}', + '
', + '
', + '', + '', + '', + '${i18n.monthlyWeekdayOfMonth2}', + '', + '${i18n.monthlyWeekdayOfMonth3}', + '
', + '
', + '
', + '
', + '', + '
', + '', + '${i18n.yearlyInterval2}', + '
', + '
', + '
', + '', + '
', + '
', + '', + '', + '', + '${i18n.yearlyDayOfMonth2}', + '', + '${i18n.yearlyDayOfMonth3}', + '
', + '
', + '', + '', + '', + '', + '', + '${i18n.yearlyWeekdayOfMonth3}', + '', + '${i18n.yearlyWeekdayOfMonth4}', + '
', + '
', + '
', + '
', + '', + '
', + '{{if hasRepeatForeverButton}}', + '
', + '', + '', + '
', + '{{/if}}', + '
', + '', + '', + '', + '${i18n.rangeByOccurrences2}', + '
', + '
', + '', + '', + '', + '
', + '
', + '
', + '
', + '
', + '
', + '

${i18n.preview}

', + '', + '', + '${i18n.refresh}', + '', + '', + '
', + '
', + '
', + '
', + '
', + '
', + '

${i18n.addDate}

', + '
', + '
', + '
', + '', + '', + '
', + '
', + + '
', + '', + '', + '
', + '
'].join('\n'); + + $.template('formTmpl', FORMTMPL); + + // Formatting function (mostly) from jQueryTools dateinput + var Re = /d{1,4}|m{1,4}|yy(?:yy)?|"[^"]*"|'[^']*'/g; + + function zeropad(val, len) { + val = val.toString(); + len = len || 2; + while (val.length < len) { val = "0" + val; } + return val; + } + + function format(date, fmt, conf) { + var d = date.getDate(), + D = date.getDay(), + m = date.getMonth(), + y = date.getFullYear(), + + flags = { + d: d, + dd: zeropad(d), + ddd: conf.i18n.shortWeekdays[D], + dddd: conf.i18n.weekdays[D], + m: m + 1, + mm: zeropad(m + 1), + mmm: conf.i18n.shortMonths[m], + mmmm: conf.i18n.months[m], + yy: String(y).slice(2), + yyyy: y + }; + + var result = fmt.replace(Re, function ($0) { + return flags.hasOwnProperty($0) ? flags[$0] : $0.slice(1, $0.length - 1); + }); + + return result; + + } + + /** + * Parsing RFC5545 from widget + */ + function widgetSaveToRfc5545(form, conf, tz) { + var value = form.find('select[name=rirtemplate]').val(); + var rtemplate = conf.rtemplate[value]; + var result = rtemplate.rrule; + var human = conf.i18n.rtemplate[value]; + var field, input, weekdays, i18nweekdays, i, j, index, tmp; + var day, month, year, interval, yearlyType, occurrences, date; + + for (i = 0; i < rtemplate.fields.length; i++) { + field = form.find('#' + rtemplate.fields[i]); + + switch (field.attr('id')) { + + case 'ridailyinterval': + interval = field.find('input[name=ridailyinterval]').val(); + if (interval !== '1') { + result += ';INTERVAL=' + interval; + } + human = interval + ' ' + conf.i18n.dailyInterval2; + break; + + case 'riweeklyinterval': + interval = field.find('input[name=riweeklyinterval]').val(); + if (interval !== '1') { + result += ';INTERVAL=' + interval; + } + human = interval + ' ' + conf.i18n.weeklyInterval2; + break; + + case 'riweeklyweekdays': + weekdays = ''; + i18nweekdays = ''; + for (j = 0; j < conf.weekdays.length; j++) { + input = field.find('input[name=riweeklyweekdays' + conf.weekdays[j] + ']'); + if (input.is(':checked')) { + if (weekdays) { + weekdays += ','; + i18nweekdays += ', '; + } + weekdays += conf.weekdays[j]; + i18nweekdays += conf.i18n.weekdays[j]; + } + } + if (weekdays) { + result += ';BYDAY=' + weekdays; + human += ' ' + conf.i18n.weeklyWeekdaysHuman + ' ' + i18nweekdays; + } + break; + + case 'rimonthlyinterval': + interval = field.find('input[name=rimonthlyinterval]').val(); + if (interval !== '1') { + result += ';INTERVAL=' + interval; + } + human = interval + ' ' + conf.i18n.monthlyInterval2; + break; + + case 'rimonthlyoptions': + var monthlyType = $('input[name=rimonthlytype]:checked', form).val(); + switch (monthlyType) { + + case 'DAYOFMONTH': + day = $('select[name=rimonthlydayofmonthday]', form).val(); + result += ';BYMONTHDAY=' + day; + human += ', ' + conf.i18n.monthlyDayOfMonth1Human + ' ' + day + ' ' + conf.i18n.monthlyDayOfMonth2; + break; + case 'WEEKDAYOFMONTH': + index = $('select[name=rimonthlyweekdayofmonthindex]', form).val(); + day = $('select[name=rimonthlyweekdayofmonth]', form).val(); + if ($.inArray(day, ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']) > -1) { + result += ';BYDAY=' + index + day; + human += ', ' + conf.i18n.monthlyWeekdayOfMonth1Human + ' '; + human += ' ' + conf.i18n.orderIndexes[$.inArray(index, conf.orderIndexes)]; + human += ' ' + conf.i18n.monthlyWeekdayOfMonth2; + human += ' ' + conf.i18n.weekdays[$.inArray(day, conf.weekdays)]; + human += ' ' + conf.i18n.monthlyDayOfMonth2; + } + break; + } + break; + + case 'riyearlyinterval': + interval = field.find('input[name=riyearlyinterval]').val(); + if (interval !== '1') { + result += ';INTERVAL=' + interval; + } + human = interval + ' ' + conf.i18n.yearlyInterval2; + break; + + case 'riyearlyoptions': + yearlyType = $('input[name=riyearlyType]:checked', form).val(); + switch (yearlyType) { + + case 'DAYOFMONTH': + month = $('select[name=riyearlydayofmonthmonth]', form).val(); + day = $('select[name=riyearlydayofmonthday]', form).val(); + result += ';BYMONTH=' + month; + result += ';BYMONTHDAY=' + day; + human += ', ' + conf.i18n.yearlyDayOfMonth1Human + ' ' + conf.i18n.months[month - 1] + ' ' + day; + break; + case 'WEEKDAYOFMONTH': + index = $('select[name=riyearlyweekdayofmonthindex]', form).val(); + day = $('select[name=riyearlyweekdayofmonthday]', form).val(); + month = $('select[name=riyearlyweekdayofmonthmonth]', form).val(); + result += ';BYMONTH=' + month; + if ($.inArray(day, ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']) > -1) { + result += ';BYDAY=' + index + day; + human += ', ' + conf.i18n.yearlyWeekdayOfMonth1Human; + human += ' ' + conf.i18n.orderIndexes[$.inArray(index, conf.orderIndexes)]; + human += ' ' + conf.i18n.yearlyWeekdayOfMonth2; + human += ' ' + conf.i18n.weekdays[$.inArray(day, conf.weekdays)]; + human += ' ' + conf.i18n.yearlyWeekdayOfMonth3; + human += ' ' + conf.i18n.months[month - 1]; + human += ' ' + conf.i18n.yearlyWeekdayOfMonth4; + } + break; + } + break; + + case 'rirangeoptions': + var rangeType = form.find('input[name=rirangetype]:checked').val(); + switch (rangeType) { + + case 'BYOCCURRENCES': + occurrences = form.find('input[name=rirangebyoccurrencesvalue]').val(); + result += ';COUNT=' + occurrences; + human += ', ' + conf.i18n.rangeByOccurrences1Human; + human += ' ' + occurrences; + human += ' ' + conf.i18n.rangeByOccurrences2; + break; + case 'BYENDDATE': + field = form.find('input[name=rirangebyenddatecalendar]'); + date = field.data('dateinput').getValue('yyyymmdd'); + result += ';UNTIL=' + date + 'T000000'; + if (tz === true) { + // Make it UTC: + result += 'Z'; + } + human += ', ' + conf.i18n.rangeByEndDateHuman; + human += ' ' + field.data('dateinput').getValue(conf.i18n.longDateFormat); + break; + } + break; + } + } + + if (form.ical.RDATE !== undefined && form.ical.RDATE.length > 0) { + form.ical.RDATE.sort(); + tmp = []; + for (i = 0; i < form.ical.RDATE.length; i++) { + if (form.ical.RDATE[i] !== '') { + year = parseInt(form.ical.RDATE[i].substring(0, 4), 10); + month = parseInt(form.ical.RDATE[i].substring(4, 6), 10) - 1; + day = parseInt(form.ical.RDATE[i].substring(6, 8), 10); + tmp.push(format(new Date(year, month, day), conf.i18n.longDateFormat, conf)); + } + } + if (tmp.length !== 0) { + human = human + conf.i18n.including + ' ' + tmp.join('; '); + } + } + + if (form.ical.EXDATE !== undefined && form.ical.EXDATE.length > 0) { + form.ical.EXDATE.sort(); + tmp = []; + for (i = 0; i < form.ical.EXDATE.length; i++) { + if (form.ical.EXDATE[i] !== '') { + year = parseInt(form.ical.EXDATE[i].substring(0, 4), 10); + month = parseInt(form.ical.EXDATE[i].substring(4, 6), 10) - 1; + day = parseInt(form.ical.EXDATE[i].substring(6, 8), 10); + tmp.push(format(new Date(year, month, day), conf.i18n.longDateFormat, conf)); + } + } + if (tmp.length !== 0) { + human = human + conf.i18n.except + ' ' + tmp.join('; '); + } + } + result = 'RRULE:' + result; + if (form.ical.EXDATE !== undefined && form.ical.EXDATE.join() !== "") { + tmp = $.map(form.ical.EXDATE, function (x) { + if (x.length === 8) { // DATE format. Make it DATE-TIME + x += 'T000000'; + } + if (tz === true) { + // Make it UTC: + x += 'Z'; + } + return x; + }); + result = result + '\nEXDATE:' + tmp; + } + if (form.ical.RDATE !== undefined && form.ical.RDATE.join() !== "") { + tmp = $.map(form.ical.RDATE, function (x) { + if (x.length === 8) { // DATE format. Make it DATE-TIME + x += 'T000000'; + } + if (tz === true) { + // Make it UTC: + x += 'Z'; + } + return x; + }); + result = result + '\nRDATE:' + tmp; + } + return {result: result, description: human}; + } + + function parseLine(icalline) { + var result = {}; + var pos = icalline.indexOf(':'); + var property = icalline.substring(0, pos); + result.value = icalline.substring(pos + 1); + + if (property.indexOf(';') !== -1) { + pos = property.indexOf(';'); + result.parameters = property.substring(pos + 1); + result.property = property.substring(0, pos); + } else { + result.parameters = null; + result.property = property; + } + return result; + } + + function cleanDates(dates) { + // Get rid of timezones + // TODO: We could parse dates and range here, maybe? + var result = []; + var splitDates = dates.split(','); + var date; + + for (date in splitDates) { + if (splitDates.hasOwnProperty(date)) { + if (splitDates[date].indexOf('Z') !== -1) { + result.push(splitDates[date].substring(0, 15)); + } else { + result.push(splitDates[date]); + } + } + } + return result; + } + + function parseIcal(icaldata) { + var lines = []; + var result = {}; + var propAndValue = []; + var line = null; + var nextline; + + lines = icaldata.split('\n'); + lines.reverse(); + while (true) { + if (lines.length > 0) { + nextline = lines.pop(); + if (nextline.charAt(0) === ' ' || nextline.charAt(0) === '\t') { + // Line continuation: + line = line + nextline; + continue; + } + } else { + nextline = ''; + } + + // New line; the current one is finished, add it to the result. + if (line !== null) { + line = parseLine(line); + // We ignore properties for now + if (line.property === 'RDATE' || line.property === 'EXDATE') { + result[line.property] = cleanDates(line.value); + } else { + result[line.property] = line.value; + } + } + + line = nextline; + if (line === '') { + break; + } + } + return result; + } + + function widgetLoadFromRfc5545(form, conf, icaldata, force) { + var unsupportedFeatures = []; + var i, matches, match, matchIndex, rtemplate, d, input, index; + var selector, selectors, field, radiobutton, start, end; + var interval, byday, bymonth, bymonthday, count, until; + var day, month, year, weekday, ical; + + form.ical = parseIcal(icaldata); + if (form.ical.RRULE === undefined) { + unsupportedFeatures.push(conf.i18n.noRule); + if (!force) { + return -1; // Fail! + } + } else { + + + matches = /INTERVAL=([0-9]+);?/.exec(form.ical.RRULE); + if (matches) { + interval = matches[1]; + } else { + interval = '1'; + } + + matches = /BYDAY=([^;]+);?/.exec(form.ical.RRULE); + if (matches) { + byday = matches[1]; + } else { + byday = ''; + } + + matches = /BYMONTHDAY=([^;]+);?/.exec(form.ical.RRULE); + if (matches) { + bymonthday = matches[1].split(","); + } else { + bymonthday = null; + } + + matches = /BYMONTH=([^;]+);?/.exec(form.ical.RRULE); + if (matches) { + bymonth = matches[1].split(","); + } else { + bymonth = null; + } + + matches = /COUNT=([0-9]+);?/.exec(form.ical.RRULE); + if (matches) { + count = matches[1]; + } else { + count = null; + } + + matches = /UNTIL=([0-9T]+);?/.exec(form.ical.RRULE); + if (matches) { + until = matches[1]; + } else { + until = null; + } + + matches = /BYSETPOS=([^;]+);?/.exec(form.ical.RRULE); + if (matches) { + unsupportedFeatures.push(conf.i18n.bysetpos); + } + + // Find the best rule: + match = ''; + matchIndex = null; + for (i in conf.rtemplate) { + if (conf.rtemplate.hasOwnProperty(i)) { + rtemplate = conf.rtemplate[i]; + if (form.ical.RRULE.indexOf(rtemplate.rrule) === 0) { + if (form.ical.RRULE.length > match.length) { + // This is the best match so far + match = form.ical.RRULE; + matchIndex = i; + } + } + } + } + + if (match) { + rtemplate = conf.rtemplate[matchIndex]; + // Set the selector: + selector = form.find('select[name=rirtemplate]').val(matchIndex); + } else { + for (rtemplate in conf.rtemplate) { + if (conf.rtemplate.hasOwnProperty(rtemplate)) { + rtemplate = conf.rtemplate[rtemplate]; + break; + } + } + unsupportedFeatures.push(conf.i18n.noTemplateMatch); + } + + for (i = 0; i < rtemplate.fields.length; i++) { + field = form.find('#' + rtemplate.fields[i]); + switch (field.attr('id')) { + + case 'ridailyinterval': + field.find('input[name=ridailyinterval]').val(interval); + break; + + case 'riweeklyinterval': + field.find('input[name=riweeklyinterval]').val(interval); + break; + + case 'riweeklyweekdays': + byday = byday.split(","); + for (d = 0; d < conf.weekdays.length; d++) { + day = conf.weekdays[d]; + input = field.find('input[name=riweeklyweekdays' + day + ']'); + input.attr('checked', $.inArray(day, byday) !== -1); + } + break; + + case 'rimonthlyinterval': + field.find('input[name=rimonthlyinterval]').val(interval); + break; + + case 'rimonthlyoptions': + var monthlyType = 'DAYOFMONTH'; // Default to using BYMONTHDAY + + if (bymonthday) { + monthlyType = 'DAYOFMONTH'; + if (bymonthday.length > 1) { + // No support for multiple days in one month + unsupportedFeatures.push(conf.i18n.multipleDayOfMonth); + // Just keep the first + bymonthday = bymonthday[0]; + } + field.find('select[name=rimonthlydayofmonthday]').val(bymonthday); + } + + if (byday) { + monthlyType = 'WEEKDAYOFMONTH'; + + if (byday.indexOf(',') !== -1) { + // No support for multiple days in one month + unsupportedFeatures.push(conf.i18n.multipleDayOfMonth); + byday = byday.split(",")[0]; + } + index = byday.slice(0, -2); + if (index.charAt(0) !== '+' && index.charAt(0) !== '-') { + index = '+' + index; + } + weekday = byday.slice(-2); + field.find('select[name=rimonthlyweekdayofmonthindex]').val(index); + field.find('select[name=rimonthlyweekdayofmonth]').val(weekday); + } + + selectors = field.find('input[name=rimonthlytype]'); + for (index = 0; index < selectors.length; index++) { + radiobutton = selectors[index]; + $(radiobutton).attr('checked', radiobutton.value === monthlyType); + } + break; + + case 'riyearlyinterval': + field.find('input[name=riyearlyinterval]').val(interval); + break; + + case 'riyearlyoptions': + var yearlyType = 'DAYOFMONTH'; // Default to using BYMONTHDAY + + if (bymonthday) { + yearlyType = 'DAYOFMONTH'; + if (bymonthday.length > 1) { + // No support for multiple days in one month + unsupportedFeatures.push(conf.i18n.multipleDayOfMonth); + bymonthday = bymonthday[0]; + } + field.find('select[name=riyearlydayofmonthmonth]').val(bymonth); + field.find('select[name=riyearlydayofmonthday]').val(bymonthday); + } + + if (byday) { + yearlyType = 'WEEKDAYOFMONTH'; + + if (byday.indexOf(',') !== -1) { + // No support for multiple days in one month + unsupportedFeatures.push(conf.i18n.multipleDayOfMonth); + byday = byday.split(",")[0]; + } + index = byday.slice(0, -2); + if (index.charAt(0) !== '+' && index.charAt(0) !== '-') { + index = '+' + index; + } + weekday = byday.slice(-2); + field.find('select[name=riyearlyweekdayofmonthindex]').val(index); + field.find('select[name=riyearlyweekdayofmonthday]').val(weekday); + field.find('select[name=riyearlyweekdayofmonthmonth]').val(bymonth); + } + + selectors = field.find('input[name=riyearlyType]'); + for (index = 0; index < selectors.length; index++) { + radiobutton = selectors[index]; + $(radiobutton).attr('checked', radiobutton.value === yearlyType); + } + break; + + case 'rirangeoptions': + var rangeType = 'NOENDDATE'; + + if (count) { + rangeType = 'BYOCCURRENCES'; + field.find('input[name=rirangebyoccurrencesvalue]').val(count); + } + + if (until) { + rangeType = 'BYENDDATE'; + input = field.find('input[name=rirangebyenddatecalendar]'); + year = until.slice(0, 4); + month = until.slice(4, 6); + month = parseInt(month, 10) - 1; + day = until.slice(6, 8); + input.data('dateinput').setValue(new Date(year, month, day)); + } + + selectors = field.find('input[name=rirangetype]'); + for (index = 0; index < selectors.length; index++) { + radiobutton = selectors[index]; + $(radiobutton).attr('checked', radiobutton.value === rangeType); + } + break; + } + } + } + + var messagearea = form.find('#messagearea'); + if (unsupportedFeatures.length !== 0) { + messagearea.text(conf.i18n.unsupportedFeatures + ' ' + unsupportedFeatures.join('; ')); + messagearea.show(); + return 1; + } else { + messagearea.text(''); + messagearea.hide(); + return 0; + } + + } + + /** + * RecurrenceInput - form, display and tools for recurrenceinput widget + */ + function RecurrenceInput(conf, textarea) { + + var self = this; + var form, display; + + // Extend conf with non-configurable data used by templates. + var orderedWeekdays = []; + var index, i; + for (i = 0; i < 7; i++) { + index = i + conf.firstDay; + if (index > 6) { + index = index - 7; + } + orderedWeekdays.push(index); + } + + $.extend(conf, { + orderIndexes: ['+1', '+2', '+3', '+4', '-1'], + weekdays: ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'], + orderedWeekdays: orderedWeekdays + }); + + // The recurrence type dropdown should show certain fields depending + // on selection: + function displayFields(selector) { + var i; + // First hide all the fields + form.find('.rifield').hide(); + // Then show the ones that should be shown. + var value = selector.val(); + if (value) { + var rtemplate = conf.rtemplate[value]; + for (i = 0; i < rtemplate.fields.length; i++) { + form.find('#' + rtemplate.fields[i]).show(); + } + } + } + + function occurrenceExclude(event) { + event.preventDefault(); + if (form.ical.EXDATE === undefined) { + form.ical.EXDATE = []; + } + form.ical.EXDATE.push(this.attributes.date.value); + var $this = $(this); + $this.attr('class', 'exdate'); + $this.parent().parent().addClass('exdate'); + $this.unbind(event); + $this.click(occurrenceInclude); // Jslint warns here, but that's OK. + } + + function occurrenceInclude(event) { + event.preventDefault(); + form.ical.EXDATE.splice($.inArray(this.attributes.date.value, form.ical.EXDATE), 1); + var $this = $(this); + $this.attr('class', 'rrule'); + $this.parent().parent().removeClass('exdate'); + $this.unbind(event); + $this.click(occurrenceExclude); + } + + function occurrenceDelete(event) { + event.preventDefault(); + form.ical.RDATE.splice($.inArray(this.attributes.date.value, form.ical.RDATE), 1); + $(this).parent().parent().hide('slow', function () { + $(this).remove(); + }); + } + + function occurrenceAdd(event) { + event.preventDefault(); + var dateinput = form + .find('.riaddoccurrence input#adddate') + .data('dateinput'); + var datevalue = dateinput.getValue('yyyymmddT000000'); + if (form.ical.RDATE === undefined) { + form.ical.RDATE = []; + } + var errorarea = form.find('.riaddoccurrence div.errorarea'); + errorarea.text(''); + errorarea.hide(); + + // Add date only if it is not already in RDATE + if ($.inArray(datevalue, form.ical.RDATE) === -1) { + form.ical.RDATE.push(datevalue); + var html = [''].join('\n'); + form.find('div.rioccurrences').prepend(html); + $(form.find('div.rioccurrences div')[0]).slideDown(); + $(form.find('div.rioccurrences .action a.rdate')[0]).click(occurrenceDelete); + } else { + errorarea.text(conf.i18n.alreadyAdded).show(); + } + } + + // element is where to find the tag in question. Can be the form + // or the display widget. Defaults to the form. + function loadOccurrences(startdate, rfc5545, start, readonly) { + var element, occurrenceDiv; + + if (!readonly) { + element = form; + } else { + element = display; + } + + occurrenceDiv = element.find('.rioccurrences'); + occurrenceDiv.hide(); + + var year, month, day; + year = startdate.getFullYear(); + month = startdate.getMonth() + 1; + day = startdate.getDate(); + + var data = {year: year, + month: month, // Sending January as 0? I think not. + day: day, + rrule: rfc5545, + format: conf.i18n.longDateFormat, + start: start}; + + var dict = { + url: conf.ajaxURL, + async: false, // Can't be tested if it's asynchronous, annoyingly. + type: 'post', + dataType: 'json', + contentType: conf.ajaxContentType, + cache: false, + data: data, + success: function (data, status, jqXHR) { + var result, element; + + if (!readonly) { + element = form; + } else { + element = display; + } + data.readOnly = readonly; + data.i18n = conf.i18n; + + // Format dates: + var occurrence, date, y, m, d, each; + for (each in data.occurrences) { + if (data.occurrences.hasOwnProperty(each)) { + occurrence = data.occurrences[each]; + date = occurrence.date; + y = parseInt(date.substring(0, 4), 10); + m = parseInt(date.substring(4, 6), 10) - 1; // jan=0 + d = parseInt(date.substring(6, 8), 10); + occurrence.formattedDate = format(new Date(y, m, d), conf.i18n.longDateFormat, conf); + } + } + + result = $.tmpl('occurrenceTmpl', data); + occurrenceDiv = element.find('.rioccurrences'); + occurrenceDiv.replaceWith(result); + + // Add the batch actions: + element.find('.rioccurrences .batching a').click( + function (event) { + event.preventDefault(); + loadOccurrences(startdate, rfc5545, this.attributes.start.value, readonly); + } + ); + + // Add the delete/undelete actions: + if (!readonly) { + element.find('.rioccurrences .action a.rrule').click(occurrenceExclude); + element.find('.rioccurrences .action a.exdate').click(occurrenceInclude); + element.find('.rioccurrences .action a.rdate').click(occurrenceDelete); + } + // Show the new div + element.find('.rioccurrences').show(); + }, + error: function (jqXHR, textStatus, errorThrown) { + alert(textStatus); + } + }; + + $.ajax(dict); + } + + function getField(field) { + // See if it is a field already + var realField = $(field); + if (!realField.length) { + // Otherwise, we assume it's an id: + realField = $('#' + field); + } + if (!realField.length) { + // Still not? Then it's a name. + realField = $("input[name='" + field + "']"); + } + return realField; + } + function findStartDate() { + var startdate = null; + var startField, startFieldYear, startFieldMonth, startFieldDay; + + // Find the default byday and bymonthday from the start date, if any: + if (conf.startField) { + startField = getField(conf.startField); + if (!startField.length) { + // Field not found + return null; + } + // Now we have a field, see if it is a dateinput field: + startdate = startField.data('dateinput'); + if (!startdate) { + //No, it wasn't, just try to interpret it with Date() + startdate = startField.val(); + if (startdate === "") { + // Probably not an input at all. Try to see if it contains a date + startdate = startField.text(); + } + } else { + // Yes it was, get the date: + startdate = startdate.getValue(); + } + + if (typeof startdate === 'string') { + // convert human readable, non ISO8601 dates, like + // '2014-04-24 19:00', where the 'T' separator is missing. + startdate = startdate.replace(' ', 'T'); + } + + startdate = new Date(startdate); + } else if (conf.startFieldYear && + conf.startFieldMonth && + conf.startFieldDay) { + startFieldYear = getField(conf.startFieldYear); + startFieldMonth = getField(conf.startFieldMonth); + startFieldDay = getField(conf.startFieldDay); + if (!startFieldYear.length && + !startFieldMonth.length && + !startFieldDay.length) { + // Field not found + return null; + } + startdate = new Date(startFieldYear.val(), + startFieldMonth.val() - 1, + startFieldDay.val()); + } + if (startdate === null) { + return null; + } + // We have some sort of startdate: + if (isNaN(startdate)) { + return null; + } + return startdate; + } + function findEndDate(form) { + var endField, enddate; + + endField = form.find('input[name=rirangebyenddatecalendar]'); + + // Now we have a field, see if it is a dateinput field: + enddate = endField.data('dateinput'); + if (!enddate) { + //No, it wasn't, just try to interpret it with Date() + enddate = endField.val(); + } else { + // Yes it was, get the date: + enddate = enddate.getValue(); + } + enddate = new Date(enddate); + + // if the end date is incorrect or the field is left empty + if (isNaN(enddate) || endField.val() === "") { + return null; + } + return enddate; + } + function findIntField(fieldName, form) { + var field, num, isInt; + + field = form.find('input[name=' + fieldName + ']'); + + num = field.val(); + + // if it's not a number or the field is left empty + if (isNaN(num) || (num.toString().indexOf('.') !== -1) || field.val() === "") { + return null; + } + return num; + } + + // Loading (populating) display and form widget with + // passed RFC5545 string (data) + function loadData(rfc5545) { + var selector, format, startdate, dayindex, day; + + if (rfc5545) { + widgetLoadFromRfc5545(form, conf, rfc5545, true); + } + + startdate = findStartDate(); + + if (startdate !== null) { + // If the date is a real date, set the defaults in the form + form.find('select[name=rimonthlydayofmonthday]').val(startdate.getDate()); + dayindex = conf.orderIndexes[Math.floor((startdate.getDate() - 1) / 7)]; + day = conf.weekdays[startdate.getDay()]; + form.find('select[name=rimonthlyweekdayofmonthindex]').val(dayindex); + form.find('select[name=rimonthlyweekdayofmonth]').val(day); + + form.find('select[name=riyearlydayofmonthmonth]').val(startdate.getMonth() + 1); + form.find('select[name=riyearlydayofmonthday]').val(startdate.getDate()); + form.find('select[name=riyearlyweekdayofmonthindex]').val(dayindex); + form.find('select[name=riyearlyweekdayofmonthday]').val(day); + form.find('select[name=riyearlyweekdayofmonthmonth]').val(startdate.getMonth() + 1); + + // Now when we have a start date, we can also do an ajax call to calculate occurrences: + loadOccurrences(startdate, widgetSaveToRfc5545(form, conf, false).result, 0, false); + + // Show the add and refresh buttons: + form.find('div.rioccurrencesactions').show(); + + } else { + // No EXDATE/RDATE support + form.find('div.rioccurrencesactions').hide(); + } + + + selector = form.find('select[name=rirtemplate]'); + displayFields(selector); + } + + function recurrenceOn() { + var RFC5545 = widgetSaveToRfc5545(form, conf, false); + var label = display.find('label[class=ridisplay]'); + label.text(conf.i18n.displayActivate + ' ' + RFC5545.description); + textarea.val(RFC5545.result).change(); + var startdate = findStartDate(); + if (startdate !== null) { + loadOccurrences(startdate, widgetSaveToRfc5545(form, conf, false).result, 0, true); + } + display.find('button[name="riedit"]').text(conf.i18n.edit_rules); + display.find('button[name="ridelete"]').show(); + } + + function recurrenceOff() { + var label = display.find('label[class=ridisplay]'); + label.text(conf.i18n.displayUnactivate); + textarea.val('').change(); // Clear the textarea. + display.find('.rioccurrences').hide(); + display.find('button[name="riedit"]').text(conf.i18n.add_rules); + display.find('button[name="ridelete"]').hide(); + } + + function checkFields(form) { + var startDate, endDate, num, messagearea; + startDate = findStartDate(); + + // Hide any error message from before + messagearea = form.find('#messagearea'); + messagearea.text(''); + messagearea.hide(); + + // Hide add field errors + form.find('.riaddoccurrence div.errorarea').text('').hide(); + + // Repeats Daily + if (form.find('#ridailyinterval').css('display') === 'block') { + // Check repeat every field + num = findIntField('ridailyinterval', form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.i18n.noRepeatEvery).show(); + return false; + } + } + + // Repeats Weekly + if (form.find('#riweeklyinterval').css('display') === 'block') { + // Check repeat every field + num = findIntField('riweeklyinterval', form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.i18n.noRepeatEvery).show(); + return false; + } + } + + // Repeats Monthly + if (form.find('#rimonthlyinterval').css('display') === 'block') { + // Check repeat every field + num = findIntField('rimonthlyinterval', form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.i18n.noRepeatEvery).show(); + return false; + } + + // Check repeat on + if (form.find('#rimonthlyoptions input:checked').length === 0) { + messagearea.text(conf.i18n.noRepeatOn).show(); + return false; + } + } + + // Repeats Yearly + if (form.find('#riyearlyinterval').css('display') === 'block') { + // Check repeat every field + num = findIntField('riyearlyinterval', form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.i18n.noRepeatEvery).show(); + return false; + } + + // Check repeat on + if (form.find('#riyearlyoptions input:checked').length === 0) { + messagearea.text(conf.i18n.noRepeatOn).show(); + return false; + } + } + + // End recurrence fields + + // If after N occurences is selected, check its value + if (form.find('input[value="BYOCCURRENCES"]:visible:checked').length > 0) { + num = findIntField('rirangebyoccurrencesvalue', form); + if (!num || num < 1 || num > 1000) { + messagearea.text(conf.i18n.noEndAfterNOccurrences).show(); + return false; + } + } + + // If end date is selected, check its value + if (form.find('input[value="BYENDDATE"]:visible:checked').length > 0) { + endDate = findEndDate(form); + if (!endDate) { + // if endDate is null that means the field is empty + messagearea.text(conf.i18n.noEndDate).show(); + return false; + } else if (endDate < startDate) { + // the end date cannot be before start date + messagearea.text(conf.i18n.pastEndDate).show(); + return false; + } + } + + return true; + } + + function save(event) { + event.preventDefault(); + // if no field errors, process the request + if (checkFields(form)) { + // close overlay + form.overlay().close(); + recurrenceOn(); + } + } + + function cancel(event) { + event.preventDefault(); + // close overlay + form.overlay().close(); + } + + function updateOccurances() { + var startDate; + startDate = findStartDate(); + + // if no field errors, process the request + if (checkFields(form)) { + loadOccurrences(startDate, + widgetSaveToRfc5545(form, conf, false).result, + 0, + false); + } + } + + /* + Load the templates + */ + + display = $.tmpl('displayTmpl', conf); + form = $.tmpl('formTmpl', conf); + + // Make an overlay and hide it + form.overlay(conf.formOverlay).hide(); + form.ical = {RDATE: [], EXDATE: []}; + + $.tools.dateinput.localize(conf.lang, { + months: LABELS[conf.lang].months.join(), + shortMonths: LABELS[conf.lang].shortMonths.join(), + days: LABELS[conf.lang].weekdays.join(), + shortDays: LABELS[conf.lang].shortWeekdays.join() + }); + + // Make the date input into a calendar dateinput() + form.find('input[name=rirangebyenddatecalendar]').dateinput({ + selectors: true, + lang: conf.lang, + format: conf.i18n.shortDateFormat, + firstDay: conf.firstDay, + yearRange: [-5, 10] + }).data('dateinput').setValue(new Date()); + + if (textarea.val()) { + var result = widgetLoadFromRfc5545(form, conf, textarea.val(), false); + if (result === -1) { + var label = display.find('label[class=ridisplay]'); + label.text(conf.i18n.noRule); + } else { + recurrenceOn(); + } + } + + /* + Do all the GUI stuff: + */ + + // When you click "Delete...", the recurrence rules should be cleared. + display.find('button[name=ridelete]').click(function (e) { + e.preventDefault(); + recurrenceOff(); + }); + + // Show form overlay when you click on the "Edit..." link + display.find('button[name=riedit]').click( + function (e) { + // Load the form to set up the right fields to show, etc. + loadData(textarea.val()); + e.preventDefault(); + form.overlay().load(); + } + ); + + // Pop up the little add form when clicking "Add" + form.find('div.riaddoccurrence input#adddate').dateinput({ + selectors: true, + lang: conf.lang, + format: conf.i18n.shortDateFormat, + firstDay: conf.firstDay, + yearRange: [-5, 10] + }).data('dateinput').setValue(new Date()); + form.find('input#addaction').click(occurrenceAdd); + + // When the reload button is clicked, reload + form.find('a.rirefreshbutton').click( + function (event) { + event.preventDefault(); + updateOccurances(); + } + ); + + // When selecting template, update what fieldsets are visible. + form.find('select[name=rirtemplate]').change( + function (e) { + displayFields($(this)); + } + ); + + // When focus goes to a drop-down, select the relevant radiobutton. + form.find('select').change( + function (e) { + $(this).parent().find('> input').click().change(); + } + ); + form.find('input[name=rirangebyoccurrencesvalue]').change( + function (e) { + $(this).parent().find('input[name=rirangetype]').click().change(); + } + ); + form.find('input[name=rirangebyenddatecalendar]').change(function () { + // Update only if the occurances are shown + $(this).parent().find('input[name=rirangetype]').click(); + if (form.find('.rioccurrencesactions:visible').length !== 0) { + updateOccurances(); + } + }); + // Update the selected dates section + form.find('input:radio, .riweeklyweekday > input, input[name=ridailyinterval], input[name=riweeklyinterval], input[name=rimonthlyinterval], input[name=riyearlyinterval]').change( + function (e) { + // Update only if the occurances are shown + if (form.find('.rioccurrencesactions:visible').length !== 0) { + updateOccurances(); + } + } + ); + + /* + Save and cancel methods: + */ + form.find('.ricancelbutton').click(cancel); + form.find('.risavebutton').click(save); + + /* + * Public API of RecurrenceInput + */ + + $.extend(self, { + display: display, + form: form, + loadData: loadData, //Used by tests. + save: save //Used by tests. + }); + + } + + + /* + * jQuery plugin implementation + */ + $.fn.recurrenceinput = function (conf) { + if (this.data('recurrenceinput')) { + // plugin already installed + return this.data('recurrenceinput'); + } + + // "compile" configuration for widget + var config = $.extend({}, tool.conf); + $.extend(config, conf); + $.extend(config, {i18n: LABELS[config.lang], name: this.attr('name')}); + + // our recurrenceinput widget instance + var recurrenceinput = new RecurrenceInput(config, this); + // hide textarea and place display widget after textarea + recurrenceinput.form.appendTo('body'); + this.after(recurrenceinput.display); + + // hide the textarea + this.hide(); + + // save the data for next call + this.data('recurrenceinput', recurrenceinput); + return recurrenceinput; + }; + +}(jQuery)); diff --git a/templates/event/create.html b/templates/event/create.html index c6abd8c..badc4fb 100644 --- a/templates/event/create.html +++ b/templates/event/create.html @@ -4,6 +4,7 @@ {% block content %}

{{ _('Create event') }}

+
{{ form.hidden_tag() }} @@ -15,6 +16,11 @@ {{ render_field_with_errors(form.name) }} {{ render_field_with_errors(form.description) }} {{ render_field_with_errors(form.start) }} + +
diff --git a/templates/events.html b/templates/events.html index 472c732..ffc0ed6 100644 --- a/templates/events.html +++ b/templates/events.html @@ -20,17 +20,17 @@ - {% for event in events %} + {% for date in dates %} - {{ event.dates[0].start | datetimeformat }} + {{ date.start | datetimeformat }} - {{ event.name }} - {% if event.verified %} + {{ date.event.name }} + {% if date.event.verified %} {% endif %} - {{ render_ooa(event.host) }} - {{ render_place(event.place) }} + {{ render_ooa(date.event.host) }} + {{ render_place(date.event.place) }} {% endfor %} diff --git a/templates/home.html b/templates/home.html index 1049910..2450eb4 100644 --- a/templates/home.html +++ b/templates/home.html @@ -6,13 +6,12 @@ Prototyp

{{ _('Hi there!') }}

- +
{% if admin_unit_members %}

{{ _('Your Admin Units') }}

diff --git a/templates/layout.html b/templates/layout.html index 7c7433a..ae07669 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -16,6 +16,7 @@ + {%- block styles %} {%- endblock styles %} @@ -24,6 +25,9 @@ + {% block title %}{{ title|default }}{% endblock title %} @@ -53,10 +57,10 @@