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 = ['