diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py index c5fb9fad..b02d2c34 100644 --- a/swh/web/browse/views/origin.py +++ b/swh/web/browse/views/origin.py @@ -1,224 +1,211 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import json from distutils.util import strtobool from django.http import HttpResponse from django.shortcuts import render from swh.web.common import service from swh.web.common.utils import ( reverse, format_utc_iso_date, parse_timestamp ) from swh.web.common.exc import handle_view_exception from swh.web.browse.utils import get_origin_visits from swh.web.browse.browseurls import browse_route from .utils.snapshot_context import ( browse_snapshot_directory, browse_snapshot_content, browse_snapshot_log, browse_snapshot_branches, browse_snapshot_releases ) @browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/directory/', # noqa r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/directory/(?P.+)/', # noqa r'origin/(?P[a-z]+)/url/(?P.+)/directory/', # noqa r'origin/(?P[a-z]+)/url/(?P.+)/directory/(?P.+)/', # noqa view_name='browse-origin-directory') def origin_directory_browse(request, origin_type, origin_url, timestamp=None, path=None): """Django view for browsing the content of a SWH directory associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/directory/[(path)/]` * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/visit/(timestamp)/directory/[(path)/]` """ # noqa return browse_snapshot_directory( request, origin_type=origin_type, origin_url=origin_url, timestamp=timestamp, path=path) @browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/content/(?P.+)/', # noqa r'origin/(?P[a-z]+)/url/(?P.+)/content/(?P.+)/', # noqa view_name='browse-origin-content') def origin_content_browse(request, origin_type, origin_url, path, timestamp=None): """Django view that produces an HTML display of a SWH content associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/content/(path)/` * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/visit/(timestamp)/content/(path)/` """ # noqa return browse_snapshot_content(request, origin_type=origin_type, origin_url=origin_url, timestamp=timestamp, path=path) PER_PAGE = 20 @browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/log/', # noqa r'origin/(?P[a-z]+)/url/(?P.+)/log/', view_name='browse-origin-log') def origin_log_browse(request, origin_type, origin_url, timestamp=None): """Django view that produces an HTML display of revisions history (aka the commit log) associated to a SWH origin. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/log/` * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/visit/(timestamp)/log/` """ # noqa return browse_snapshot_log(request, origin_type=origin_type, origin_url=origin_url, timestamp=timestamp) @browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/branches/', # noqa r'origin/(?P[a-z]+)/url/(?P.+)/branches/', # noqa view_name='browse-origin-branches') def origin_branches_browse(request, origin_type, origin_url, timestamp=None): """Django view that produces an HTML display of the list of branches associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/branches/` * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/visit/(timestamp)/branches/` """ # noqa return browse_snapshot_branches(request, origin_type=origin_type, origin_url=origin_url, timestamp=timestamp) @browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/releases/', # noqa r'origin/(?P[a-z]+)/url/(?P.+)/releases/', # noqa view_name='browse-origin-releases') def origin_releases_browse(request, origin_type, origin_url, timestamp=None): """Django view that produces an HTML display of the list of releases associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/releases/` * :http:get:`/browse/origin/(origin_type)/url/(origin_url)/visit/(timestamp)/releases/` """ # noqa return browse_snapshot_releases(request, origin_type=origin_type, origin_url=origin_url, timestamp=timestamp) @browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/', view_name='browse-origin') def origin_browse(request, origin_type=None, origin_url=None): """Django view that produces an HTML display of a swh origin identified by its id or its url. The url that points to it is :http:get:`/browse/origin/(origin_type)/url/(origin_url)/`. """ # noqa try: origin_info = service.lookup_origin({ 'type': origin_type, 'url': origin_url }) origin_visits = get_origin_visits(origin_info) - origin_visits.reverse() except Exception as exc: return handle_view_exception(request, exc) origin_info['last swh visit browse url'] = \ reverse('browse-origin-directory', kwargs={'origin_type': origin_type, 'origin_url': origin_url}) - origin_visits_data = [] - visits_splitted = [] - visits_by_year = {} for i, visit in enumerate(origin_visits): - visit_date = parse_timestamp(visit['date']) - visit_year = str(visit_date.year) url_date = format_utc_iso_date(visit['date'], '%Y-%m-%dT%H:%M:%SZ') visit['fmt_date'] = format_utc_iso_date(visit['date']) query_params = {} if i < len(origin_visits) - 1: if visit['date'] == origin_visits[i+1]['date']: query_params = {'visit_id': visit['visit']} if i > 0: if visit['date'] == origin_visits[i-1]['date']: query_params = {'visit_id': visit['visit']} + snapshot = visit['snapshot'] if visit['snapshot'] else '' + visit['browse_url'] = reverse('browse-origin-directory', kwargs={'origin_type': origin_type, 'origin_url': origin_url, 'timestamp': url_date}, query_params=query_params) - origin_visits_data.insert(0, {'date': visit_date.timestamp()}) - if visit_year not in visits_by_year: - # display 3 years by row in visits list view - if len(visits_by_year) == 3: - visits_splitted.append(visits_by_year) - visits_by_year = {} - visits_by_year[visit_year] = [] - visits_by_year[visit_year].append(visit) - - if len(visits_by_year) > 0: - visits_splitted.append(visits_by_year) + if not snapshot: + visit['snapshot'] = '' + visit['date'] = parse_timestamp(visit['date']).timestamp() return render(request, 'origin.html', {'empty_browse': False, 'heading': 'Origin information', 'top_panel_visible': False, 'top_panel_collapsible': False, - 'top_panel_text': 'SWH object: Visits history', + 'top_panel_text': 'SWH origin visits', 'swh_object_metadata': origin_info, 'main_panel_visible': True, - 'origin_visits_data': origin_visits_data, - 'visits_splitted': visits_splitted, + 'origin_visits': origin_visits, 'origin_info': origin_info, 'browse_url_base': '/browse/origin/%s/url/%s/' % (origin_type, origin_url), 'vault_cooking': None, 'show_actions_menu': False}) @browse_route(r'origin/search/(?P.+)/', view_name='browse-origin-search') def _origin_search(request, url_pattern): """Internal browse endpoint to search for origins whose urls contain a provided string pattern or match a provided regular expression. The search is performed in a case insensitive way. """ offset = int(request.GET.get('offset', '0')) limit = int(request.GET.get('limit', '50')) regexp = request.GET.get('regexp', 'false') results = service.search_origin(url_pattern, offset, limit, bool(strtobool(regexp))) results = json.dumps(list(results), sort_keys=True, indent=4, separators=(',', ': ')) return HttpResponse(results, content_type='application/json') @browse_route(r'origin/(?P[0-9]+)/latest_snapshot/', view_name='browse-origin-latest-snapshot') def _origin_latest_snapshot(request, origin_id): """ Internal browse endpoint used to check if an origin has already been visited by Software Heritage and has at least one full visit. """ result = service.lookup_latest_origin_snapshot(origin_id, allowed_statuses=['full']) result = json.dumps(result, sort_keys=True, indent=4, separators=(',', ': ')) return HttpResponse(result, content_type='application/json') diff --git a/swh/web/static/css/bootstrap-year-calendar.css b/swh/web/static/css/bootstrap-year-calendar.css new file mode 100644 index 00000000..835b881b --- /dev/null +++ b/swh/web/static/css/bootstrap-year-calendar.css @@ -0,0 +1,263 @@ +/* ========================================================= + * Bootstrap year calendar v1.1.0 + * Repo: https://github.com/Paul-DS/bootstrap-year-calendar + * ========================================================= + * Created by Paul David-Sivelle + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + +/* Main */ +.calendar { + padding: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + direction: ltr; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.calendar:after { + /* Apply the right height on the calendar div, even if the months elements are floating */ + clear: both; + content: ""; + display:block; +} + +.calendar.calendar-rtl { + direction: rtl; +} +.calendar.calendar-rtl table tr td span { + float: right; +} + +.calendar table { + margin: auto; +} + +.calendar table td, +.calendar table th { + text-align: center; + width: 20px; + height: 20px; + border: none; + padding: 4px 5px; + font-size:12px; +} + +/* Header */ +.calendar .calendar-header +{ + width:100%; + margin-bottom:20px; +} + +.calendar .calendar-header table +{ + width:100%; +} + +.calendar .calendar-header table th +{ + font-size:22px; + padding:5px 10px; +} + +.calendar .calendar-header table th:hover { + background: #eeeeee; + cursor: pointer; +} + +.calendar .calendar-header table th.disabled, +.calendar .calendar-header table th.disabled:hover { + background: none; + cursor: default; + color:white; +} + +.calendar .calendar-header table th.prev, +.calendar .calendar-header table th.next +{ + width:20px; +} + +.calendar .year-title { + font-weight:bold; + text-align:center; + height:20px; + width:auto; +} + +.calendar .year-neighbor +{ + color:#aaaaaa; +} + +.calendar .year-neighbor2 +{ + color:#dddddd; +} + +/* Months */ +.calendar .months-container { + width:100%; + display:none; +} + +.calendar .month-container { + text-align:center; + height:200px; + padding:0; +} + +.calendar table.month th.month-title +{ + font-size:16px; + padding-bottom: 5px; +} + +.calendar table.month th.day-header +{ + font-size:14px; +} + + +.calendar table.month tr td, +.calendar table.month tr th +{ + padding:0; +} + +.calendar table.month tr td.hidden, +.calendar table.month tr th.hidden +{ + display:none; +} + +.calendar table.month td.week-number { + cursor: default; + font-weight:bold; + border-right:1px solid #eee; + padding:5px; +} + +.calendar .round-left { + -webkit-border-radius: 8px 0 0 8px; + -moz-border-radius: 8px 0 0 8px; + border-radius: 8px 0 0 8px; +} + +.calendar .round-right { + webkit-border-radius: 0 8px 8px 0 ; + -moz-border-radius: 0 8px 8px 0; + border-radius: 0 8px 8px 0; +} + +.calendar table.month tr td .day-content { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + padding: 5px 6px; +} + +.table-striped .calendar table.month tr td, +.table-striped .calendar table.month tr th { + background-color: transparent; +} + +.calendar table.month td.day .day-content:hover { + background: rgba(0, 0, 0, 0.2); + cursor: pointer; +} +.calendar table.month tr td.old, +.calendar table.month tr td.new, +.calendar table.month tr td.old:hover, +.calendar table.month tr td.new:hover { + background: none; + cursor: default; +} +.calendar table.month tr td.disabled, +.calendar table.month tr td.disabled:hover { + color: #dddddd; +} + +.calendar table.month td.day.disabled .day-content:hover { + background: none; + cursor: default; +} + +.calendar table.month tr td.range .day-content { + background: rgba(0, 0, 0, 0.2); + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.calendar table.month tr td.range.range-start .day-content { + border-top-left-radius:4px; + border-bottom-left-radius:4px; +} + +.calendar table.month tr td.range.range-end .day-content { + border-top-right-radius:4px; + border-bottom-right-radius:4px; +} + +.calendar-context-menu, +.calendar-context-menu .submenu { + border: 1px solid #ddd; + background-color: white; + box-shadow: 2px 2px 5px rgba(0, 0, 0, .2); + -webkit-box-shadow: 2px 2px 5px rgba(0, 0, 0, .2); + position:absolute; + display:none; +} + +.calendar-context-menu .item { + padding:5px 10px; + cursor:pointer; + display:table; + width:100%; +} + +.calendar-context-menu .item:hover { + background:#eee; +} + +.calendar-context-menu .item .content { + display:table-cell; +} + +.calendar-context-menu .item span { + display:table-cell; + padding-left:10px; + text-align:right; +} + +.calendar-context-menu .item span:last-child { + display:none; +} + +.calendar-context-menu .submenu { + left: 100%; + margin-top: -6px; +} + +.calendar-context-menu .item:hover > .submenu { + display:block; +} diff --git a/swh/web/static/css/bootstrap-year-calendar.min.css b/swh/web/static/css/bootstrap-year-calendar.min.css new file mode 100644 index 00000000..e2b05b6b --- /dev/null +++ b/swh/web/static/css/bootstrap-year-calendar.min.css @@ -0,0 +1,7 @@ +/* + * Bootstrap year calendar v1.1.0 + * Created by Paul David-Sivelle + * Licensed under the Apache License, Version 2.0 + */ + + .calendar{padding:4px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;direction:ltr;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.calendar:after{clear:both;content:"";display:block}.calendar.calendar-rtl{direction:rtl}.calendar.calendar-rtl table tr td span{float:right}.calendar table{margin:auto}.calendar table td,.calendar table th{text-align:center;width:20px;height:20px;border:none;padding:4px 5px;font-size:12px}.calendar .calendar-header{width:100%;margin-bottom:20px}.calendar .calendar-header table{width:100%}.calendar .calendar-header table th{font-size:22px;padding:5px 10px}.calendar .calendar-header table th:hover{background:#eee;cursor:pointer}.calendar .calendar-header table th.disabled,.calendar .calendar-header table th.disabled:hover{background:0 0;cursor:default;color:#fff}.calendar .calendar-header table th.next,.calendar .calendar-header table th.prev{width:20px}.calendar .year-title{font-weight:700;text-align:center;height:20px;width:auto}.calendar .year-neighbor{color:#aaa}.calendar .year-neighbor2,.calendar table.month tr td.disabled,.calendar table.month tr td.disabled:hover{color:#ddd}.calendar .months-container{width:100%;display:none}.calendar .month-container{text-align:center;height:200px;padding:0}.calendar table.month th.month-title{font-size:16px;padding-bottom:5px}.calendar table.month th.day-header{font-size:14px}.calendar table.month tr td,.calendar table.month tr th{padding:0}.calendar table.month tr td.hidden,.calendar table.month tr th.hidden{display:none}.calendar table.month td.week-number{cursor:default;font-weight:700;border-right:1px solid #eee;padding:5px}.calendar .round-left{-webkit-border-radius:8px 0 0 8px;-moz-border-radius:8px 0 0 8px;border-radius:8px 0 0 8px}.calendar .round-right{webkit-border-radius:0 8px 8px 0;-moz-border-radius:0 8px 8px 0;border-radius:0 8px 8px 0}.calendar table.month tr td .day-content{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;padding:5px 6px}.table-striped .calendar table.month tr td,.table-striped .calendar table.month tr th{background-color:transparent}.calendar table.month td.day .day-content:hover{background:rgba(0,0,0,.2);cursor:pointer}.calendar table.month td.day.disabled .day-content:hover,.calendar table.month tr td.new,.calendar table.month tr td.new:hover,.calendar table.month tr td.old,.calendar table.month tr td.old:hover{background:0 0;cursor:default}.calendar table.month tr td.range .day-content{background:rgba(0,0,0,.2);-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.calendar table.month tr td.range.range-start .day-content{border-top-left-radius:4px;border-bottom-left-radius:4px}.calendar table.month tr td.range.range-end .day-content{border-top-right-radius:4px;border-bottom-right-radius:4px}.calendar-context-menu,.calendar-context-menu .submenu{border:1px solid #ddd;background-color:#fff;box-shadow:2px 2px 5px rgba(0,0,0,.2);-webkit-box-shadow:2px 2px 5px rgba(0,0,0,.2);position:absolute;display:none}.calendar-context-menu .item{padding:5px 10px;cursor:pointer;display:table;width:100%}.calendar-context-menu .item:hover{background:#eee}.calendar-context-menu .item .content{display:table-cell}.calendar-context-menu .item span{display:table-cell;padding-left:10px;text-align:right}.calendar-context-menu .item span:last-child{display:none}.calendar-context-menu .submenu{left:100%;margin-top:-6px}.calendar-context-menu .item:hover>.submenu{display:block} \ No newline at end of file diff --git a/swh/web/static/css/style.css b/swh/web/static/css/style.css index 12b0c3a6..b3bf9a3e 100644 --- a/swh/web/static/css/style.css +++ b/swh/web/static/css/style.css @@ -1,799 +1,858 @@ /* version: 0.1 date: 21/09/15 author: swh email: swh website: softwareheritage.org version history: /style.css */ @import url(https://fonts.googleapis.com/css?family=Alegreya:400,400italic,700,700italic); @import url(https://fonts.googleapis.com/css?family=Alegreya+Sans:400,400italic,500,500italic,700,700italic,100,300,100italic,300italic); html { height: 100%; overflow-x: hidden; } body { font-family: 'Alegreya Sans', sans-serif; font-size: 1.7rem; line-height: 1.5; color: rgba(0, 0, 0, 0.55); padding-bottom: 120px; min-height: 100%; margin: 0; position: relative; } .heading { font-family: 'Alegreya', serif; } .shell, .text { font-size: 0.7em; } .logo img { max-height: 40px; } .logo .navbar-brand { padding: 5px; } .logo .sitename { padding: 15px 5px; } .jumbotron { padding: 0; background-color: rgba(0, 0, 0, 0); position: fixed; top: 0; width: 100%; z-index: 10; } #swh-navbar-collapse { border-top-style: none; border-left-style: none; border-right-style: none; border-bottom: 5px solid; border-image: linear-gradient(to right, rgb(226, 0, 38) 0%, rgb(254, 205, 27) 100%) 1 1 1 1; width: 100%; padding: 5px; } .nav-horizontal { float: right; } h3[id], h4[id], a[id] { /* avoid in-page links covered by navbar */ padding-top: 80px; margin-top: -70px; } h1, h2, h3, h4 { margin: 0; color: #e20026; padding-bottom: 10px; } h1 { font-size: 1.8em; } h2 { font-size: 1.2em; } h3 { font-size: 1.1em; } a { color: rgba(0, 0, 0, 0.75); border-bottom-style: none; border-bottom-width: 1px; border-bottom-color: rgb(91, 94, 111); outline: none; } a:hover { color: black; } ul.dropdown-menu a, .navbar-header a, ul.navbar-nav a { /* No decoration on links in dropdown menu */ border-bottom-style: none; color: #323232; font-weight: 700; } .navbar-header a:hover, ul.navbar-nav a:hover { color: #8f8f8f; } .sitename .first-word, .sitename .second-word { color: rgba(0, 0, 0, 0.75); font-weight: normal; font-size: 1.8rem; } .sitename .first-word { font-family: 'Alegreya Sans', sans-serif; } .sitename .second-word { font-family: 'Alegreya', serif; } ul.dropdown-menu > li, ul.dropdown-menu > li > ul > li { /* No decoration on bullet points in dropdown menu */ list-style-type: none; } .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; } .entries { list-style: none; margin: 0; padding: 0; } .entries li { margin: 0.8em 1.2em; } .entries li h2 { margin-left: -1em; } .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } .add-entry dl { font-weight: bold; } .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; } .flash { background: #cee5F5; padding: 0.5em; border: 1px solid #aacbe2; } .error { background: #f0d6d6; padding: 0.5em; } .file-found { color: #23BA49; } .file-notfound { color: #FF4747; } /* Bootstrap custom styling to correctly render multiple * form-controls in an input-group: * github.com/twbs/bootstrap/issues/12732 */ .input-group-field { display: table-cell; vertical-align: middle; border-radius:4px; min-width:1%; white-space: nowrap; } .input-group-field .form-control { border-radius: inherit !important; } .input-group-field:not(:first-child):not(:last-child) { border-radius:0; } .input-group-field:not(:first-child):not(:last-child) .form-control { border-left-width: 0; border-right-width: 0; } .input-group-field:last-child { border-top-left-radius:0; border-bottom-left-radius:0; } .input-group > span:not(:last-child) > button { border-radius: 0; } .multi-input-group > .input-group-btn { vertical-align: bottom; padding: 0; } .dataTables_filter { margin-top: 15px; } .dataTables_filter input { width: 70%; float: right; } tr.api-doc-route-upcoming > td, tr.api-doc-route-upcoming > td > a { font-size: 90%; } tr.api-doc-route-deprecated > td, tr.api-doc-route-deprecated > td > a { color: red; } #back-to-top { display: initial; position: fixed; bottom: 30px; right: 30px; z-index: 10; } #back-to-top a img { display: block; width: 32px; height: 32px; background-size: 32px 32px; text-indent: -999px; overflow: hidden; } .table > thead > tr > th { border-bottom: 1px solid #e20026; } .table > tbody > tr > td { border-style: none; } pre { background-color: #f5f5f5; } .dataTables_wrapper { position: static; } /* breadcrumbs */ .bread-crumbs{ display: inline-block; overflow: hidden; color: rgba(0, 0, 0, 0.55); } bread-crumbs ul { list-style-type: none; } .bread-crumbs li { float: left; list-style-type: none; } .bread-crumbs a { color: rgba(0, 0, 0, 0.75); border-bottom-style: none; } .bread-crumbs a:hover { color: rgba(0, 0, 0, 0.85); text-decoration: underline; } .title-small .bread-crumbs{ margin: -30px 0 25px; } #footer { background-color: #262626; color: hsl(0, 0%, 100%); font-size: 1.2rem; text-align: center; padding-top: 20px; padding-bottom: 20px; position: absolute; bottom: 0; left: 0; right: 0; } #footer a, #footer a:visited { color: hsl(0, 0%, 100%); } #footer a:hover { text-decoration: underline; } .highlightjs pre { background-color: transparent; border-radius: 0px; border-color: transparent; } .hljs { background-color: transparent; white-space: pre; } .scrollable-menu { max-height: 180px; overflow-x: hidden; } .swh-browse-top-navigation { border-bottom: 1px solid #ddd; min-height: 42px; padding: 4px 5px 0px 5px; } .swh-browse-bread-crumbs { font-size: inherit; vertical-align: text-top; margin-bottom: 1px; } .swh-browse-bread-crumbs li:nth-child(n+2)::before { content: ""; display: inline-block; margin: 0 2px; } .swh-metadata-table-row { border-top: 1px solid #ddd !important; } .swh-metadata-table-key { min-width: 200px; max-width: 200px; width: 200px; } .swh-metadata-table-value pre { white-space: pre-wrap; } /* for block of numbers */ td.hljs-ln-numbers { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; text-align: center; color: #aaa; border-right: 1px solid #CCC; vertical-align: top; padding-right: 5px; /* your custom style here */ } /* for block of code */ td.hljs-ln-code { padding-left: 10px; width: 100%; } .btn-swh { color: #6C6C6C; background-color: #EAEAEA; border-color: #ddd; background-image: linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%); background-repeat: repeat-x; outline: none; } .btn-swh:hover, .btn-swh:focus, .btn-swh:active, .btn-swh.active, .open .dropdown-toggle.btn-swh { background-color: #e6ebf1; background-image: linear-gradient(to bottom,#f1f1f1 0,#e6e6e6 100%); border-color: rgb(197, 197, 197); } .btn-swh.disabled, .btn-swh[disabled], fieldset[disabled] .btn-swh, .btn-swh.disabled:hover, .btn-swh[disabled]:hover, fieldset[disabled] .btn-swh:hover, .btn-swh.disabled:focus, .btn-swh[disabled]:focus, fieldset[disabled] .btn-swh:focus, .btn-swh.disabled:active, .btn-swh[disabled]:active, fieldset[disabled] .btn-swh:active, .btn-swh.disabled.active, .btn-swh[disabled].active, fieldset[disabled] .btn-swh.active { background-color: #EAEAEA; border-color: #EAEAEA; } .btn-swh .badge { color: #EAEAEA; background-color: #6C6C6C; } .btn-swh a { color: #6C6C6C; border: none; outline: none; text-decoration: none; } .swh-http-error { margin: 0 auto; text-align: center; } .swh-http-error-head { color: #2d353c; font-size: 30px; } .swh-http-error-code { bottom: 60%; color: #2d353c; font-size: 96px; line-height: 80px; margin-bottom: 10px!important; } .swh-http-error-desc { font-size: 12px; color: #647788; text-align: center; } .swh-http-error-desc pre { display: inline-block; text-align: left; max-width: 800px; white-space: pre-wrap; } .swh-vault-table { border-bottom: none !important; margin-bottom: 0px !important; } .swh-vault-table td { vertical-align: middle !important; border-top: 1px solid #ddd !important; } .swh-counter { font-size: 150%; } .swh-loading { display : none; } .swh-loading.show { display:inline-block; position: fixed; background: white; border: 1px solid black; top: 50%; left: 50%; margin: -50px 0px 0px -50px; text-align: center; z-index:100; } .swh-readme a { outline: none; border: none; } .swh-readme table { border-collapse: collapse; } .swh-readme table, .swh-readme table th, .swh-readme table td { padding: 6px 13px; border: 1px solid #dfe2e5; } .swh-readme table tr:nth-child(even) { background-color: #f2f2f2; } .swh-web-app-link:hover { background-color: #efeff2; } .swh-web-app-link a { text-decoration: none; outline: none; border: none; } .popover { max-width: 100%; } .btn-swh-vault { border-top-right-radius: 4px !important; border-bottom-right-radius: 4px !important; } .pager a { outline: none; } .swh-content { background-image: none; border: none; background-color: white; padding: 0px; } .swh-content pre, .swh-content pre code { margin: 0px; padding: 0px; } .swh-visit-full { color: green; position: relative; } .swh-visit-full:before { content: "\f00c"; font-family: FontAwesome; left:-20px; position:absolute; top:-2px; } .swh-visit-partial { color: #edc344; position: relative; } .swh-visit-partial:before { content: "\f071"; font-family: FontAwesome; left:-20px; position:absolute; top:-2px; } .swh-visit-failed { color: #ff0000; position: relative; } .swh-visit-failed:before { content: "\f06a"; font-family: FontAwesome; left:-20px; position:absolute; top:-2px; } .swh-visit-ongoing { color: #0000ff; position: relative; } .swh-visit-ongoing:before { content: "\f021"; font-family: FontAwesome; left:-20px; position:absolute; top:-2px; } .swh-branches-releases { min-width: 200px; } .swh-branches-switch, .swh-releases-switch { padding: 5px 15px !important; } li.swh-branch:hover, li.swh-release:hover { background-color: #e8e8e8; } .nav a, .swh-branch a, .swh-release a { outline: none; } .swh-branch a:hover, .swh-release a:hover { text-decoration: none; } .swh-origin-visit-details, .swh-snapshot-details { text-align: center; } .swh-origin-visit-details ul, .swh-snapshot-details ul { list-style: none; margin: 0; padding: 0; } .swh-origin-visit-details li, .swh-snapshot-details li { display: inline-block; vertical-align: middle; margin-left: 10px; margin-right: 10px; } .swh-browse-nav li a { border-radius: 4px; } .swh-corner-ribbon { width: 200px; background: #e43; position: absolute; top: 25px; left: -50px; text-align: center; line-height: 50px; letter-spacing: 1px; color: #f0f0f0; transform: rotate(-45deg); -webkit-transform: rotate(-45deg); box-shadow: 0 0 3px rgba(0,0,0,.3); top: 25px; right: -50px; left: auto; transform: rotate(45deg); -webkit-transform: rotate(45deg); z-index: 2000; } .modal { text-align: center; padding: 0!important; } .modal:before { content: ''; display: inline-block; height: 100%; vertical-align: middle; margin-right: -4px; } .modal-dialog { display: inline-block; text-align: left; vertical-align: middle; } .panel { margin-bottom: 0px; } .panel-group { margin-bottom: 0px; } .swh-table-even-odd tr:nth-child(even) { background-color: #f5f5f5; } .swh-table-even-odd tr:nth-child(odd) { background-color: #fff; } .swh-revision-log-entry-id { min-width: 100px; max-width: 100px; width: 100px; } .swh-revision-log-entry-author { min-width: 160px; max-width: 160px; width: 160px; } .swh-table-cell-text-overflow { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .swh-log-entry-message { min-width: 520px; max-width: 520px; width: 520px; } .swh-revision-log-entry-date { min-width: 220px; max-width: 220px; width: 220px; } .swh-directory-table { margin-bottom: 0px; } .swh-directory-table td { border-top: 1px solid #ddd !important; } .swh-title-color { color: #e20026; } .swh-diff-lines-info { background-color: rgba(0, 0, 255, 0.1) !important; } .swh-diff-added-line { background-color: rgba(0, 255, 0, 0.1) !important; } .swh-diff-removed-line { background-color: rgba(255, 0, 0, 0.1) !important; } span.no-nl-marker { position: relative; color: #cb2431; vertical-align: middle; } span.no-nl-marker svg { vertical-align: text-bottom; } span.no-nl-marker svg path { fill: currentColor; } .dropdown-submenu { position: relative; } .dropdown-submenu .dropdown-menu { top: 0; left: -100%; margin-top: -6px; margin-left: -2px; } a.dropdown-left:before { content: "\f0d9"; font-family: FontAwesome; display: block; width: 20px; height: 20px; float: left; margin-left: -20px; } +#swh-visits-calendar.calendar table td { + width: 28px; + height: 28px; + padding: 0px; +} + +.d3-wrapper { + position: relative; + height: 0; + width: 100%; + padding: 0; + /* padding-bottom will be overwritten by JavaScript later */ + padding-bottom: 100%; +} +.d3-wrapper > svg { + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; +} + +svg .grid line { + stroke: lightgrey; + stroke-opacity: 0.7; + shape-rendering: crispEdges; +} + +svg .grid path { + stroke-width: 0; +} + +div.d3-tooltip { + position: absolute; + text-align: center; + width: auto; + height: auto; + padding: 2px; + font: 12px sans-serif; + background: white; + border: 1px solid black; + border-radius: 4px; + pointer-events: none; +} + +.swh-visits-list-column { + float: left; + padding: 10px; +} + +.swh-visits-list-row { + padding-left: 50px; +} + +.swh-visits-list-row:after { + content: ""; + display: table; + clear: both; +} diff --git a/swh/web/static/js/bootstrap-year-calendar/LICENSE b/swh/web/static/js/bootstrap-year-calendar/LICENSE new file mode 100644 index 00000000..8f71f43f --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/swh/web/static/js/bootstrap-year-calendar/bootstrap-year-calendar.js b/swh/web/static/js/bootstrap-year-calendar/bootstrap-year-calendar.js new file mode 100644 index 00000000..67e94669 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/bootstrap-year-calendar.js @@ -0,0 +1,1110 @@ +/* ========================================================= + * Bootstrap year calendar v1.1.0 + * Repo: https://github.com/Paul-DS/bootstrap-year-calendar + * ========================================================= + * Created by Paul David-Sivelle + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + (function($) { + var Calendar = function(element, options) { + this.element = element; + this.element.addClass('calendar'); + + this._initializeEvents(options); + this._initializeOptions(options); + this.setYear(this.options.startYear); + }; + + Calendar.prototype = { + constructor: Calendar, + _initializeOptions: function(opt) { + if(opt == null) { + opt = []; + } + + this.options = { + startYear: !isNaN(parseInt(opt.startYear)) ? parseInt(opt.startYear) : new Date().getFullYear(), + minDate: opt.minDate instanceof Date ? opt.minDate : null, + maxDate: opt.maxDate instanceof Date ? opt.maxDate : null, + language: (opt.language != null && dates[opt.language] != null) ? opt.language : 'en', + allowOverlap: opt.allowOverlap != null ? opt.allowOverlap : true, + displayWeekNumber: opt.displayWeekNumber != null ? opt.displayWeekNumber : false, + displayDisabledDataSource: opt.displayDisabledDataSource != null ? opt.displayDisabledDataSource : false, + displayHeader: opt.displayHeader != null ? opt.displayHeader : true, + alwaysHalfDay: opt.alwaysHalfDay != null ? opt.alwaysHalfDay : false, + enableRangeSelection: opt.enableRangeSelection != null ? opt.enableRangeSelection : false, + disabledDays: opt.disabledDays instanceof Array ? opt.disabledDays : [], + disabledWeekDays: opt.disabledWeekDays instanceof Array ? opt.disabledWeekDays : [], + hiddenWeekDays: opt.hiddenWeekDays instanceof Array ? opt.hiddenWeekDays : [], + roundRangeLimits: opt.roundRangeLimits != null ? opt.roundRangeLimits : false, + dataSource: opt.dataSource instanceof Array ? opt.dataSource : [], + style: opt.style == 'background' || opt.style == 'border' || opt.style == 'custom' ? opt.style : 'border', + enableContextMenu: opt.enableContextMenu != null ? opt.enableContextMenu : false, + contextMenuItems: opt.contextMenuItems instanceof Array ? opt.contextMenuItems : [], + customDayRenderer : $.isFunction(opt.customDayRenderer) ? opt.customDayRenderer : null, + customDataSourceRenderer : $.isFunction(opt.customDataSourceRenderer) ? opt.customDataSourceRenderer : null, + weekStart: !isNaN(parseInt(opt.weekStart)) ? parseInt(opt.weekStart) : null + }; + + this._initializeDatasourceColors(); + }, + _initializeEvents: function(opt) { + if(opt == null) { + opt = []; + } + + if(opt.yearChanged) { this.element.bind('yearChanged', opt.yearChanged); } + if(opt.renderEnd) { this.element.bind('renderEnd', opt.renderEnd); } + if(opt.clickDay) { this.element.bind('clickDay', opt.clickDay); } + if(opt.dayContextMenu) { this.element.bind('dayContextMenu', opt.dayContextMenu); } + if(opt.selectRange) { this.element.bind('selectRange', opt.selectRange); } + if(opt.mouseOnDay) { this.element.bind('mouseOnDay', opt.mouseOnDay); } + if(opt.mouseOutDay) { this.element.bind('mouseOutDay', opt.mouseOutDay); } + }, + _initializeDatasourceColors: function() { + for(var i = 0; i < this.options.dataSource.length; i++) { + if(this.options.dataSource[i].color == null) { + this.options.dataSource[i].color = colors[i % colors.length]; + } + } + }, + render: function() { + this.element.empty(); + + if(this.options.displayHeader) { + this._renderHeader(); + } + + this._renderBody(); + this._renderDataSource(); + + this._applyEvents(); + this.element.find('.months-container').fadeIn(500); + + this._triggerEvent('renderEnd', { currentYear: this.options.startYear }); + }, + _renderHeader: function() { + var header = $(document.createElement('div')); + header.addClass('calendar-header panel panel-default'); + + var headerTable = $(document.createElement('table')); + + var prevDiv = $(document.createElement('th')); + prevDiv.addClass('prev'); + + if(this.options.minDate != null && this.options.minDate > new Date(this.options.startYear - 1, 11, 31)) { + prevDiv.addClass('disabled'); + } + + var prevIcon = $(document.createElement('span')); + prevIcon.addClass('glyphicon glyphicon-chevron-left'); + + prevDiv.append(prevIcon); + + headerTable.append(prevDiv); + + var prev2YearDiv = $(document.createElement('th')); + prev2YearDiv.addClass('year-title year-neighbor2 hidden-sm hidden-xs'); + prev2YearDiv.text(this.options.startYear - 2); + + if(this.options.minDate != null && this.options.minDate > new Date(this.options.startYear - 2, 11, 31)) { + prev2YearDiv.addClass('disabled'); + } + + headerTable.append(prev2YearDiv); + + var prevYearDiv = $(document.createElement('th')); + prevYearDiv.addClass('year-title year-neighbor hidden-xs'); + prevYearDiv.text(this.options.startYear - 1); + + if(this.options.minDate != null && this.options.minDate > new Date(this.options.startYear - 1, 11, 31)) { + prevYearDiv.addClass('disabled'); + } + + headerTable.append(prevYearDiv); + + var yearDiv = $(document.createElement('th')); + yearDiv.addClass('year-title'); + yearDiv.text(this.options.startYear); + + headerTable.append(yearDiv); + + var nextYearDiv = $(document.createElement('th')); + nextYearDiv.addClass('year-title year-neighbor hidden-xs'); + nextYearDiv.text(this.options.startYear + 1); + + if(this.options.maxDate != null && this.options.maxDate < new Date(this.options.startYear + 1, 0, 1)) { + nextYearDiv.addClass('disabled'); + } + + headerTable.append(nextYearDiv); + + var next2YearDiv = $(document.createElement('th')); + next2YearDiv.addClass('year-title year-neighbor2 hidden-sm hidden-xs'); + next2YearDiv.text(this.options.startYear + 2); + + if(this.options.maxDate != null && this.options.maxDate < new Date(this.options.startYear + 2, 0, 1)) { + next2YearDiv.addClass('disabled'); + } + + headerTable.append(next2YearDiv); + + var nextDiv = $(document.createElement('th')); + nextDiv.addClass('next'); + + if(this.options.maxDate != null && this.options.maxDate < new Date(this.options.startYear + 1, 0, 1)) { + nextDiv.addClass('disabled'); + } + + var nextIcon = $(document.createElement('span')); + nextIcon.addClass('glyphicon glyphicon-chevron-right'); + + nextDiv.append(nextIcon); + + headerTable.append(nextDiv); + + header.append(headerTable); + + this.element.append(header); + }, + _renderBody: function() { + var monthsDiv = $(document.createElement('div')); + monthsDiv.addClass('months-container'); + + for(var m = 0; m < 12; m++) { + /* Container */ + var monthDiv = $(document.createElement('div')); + monthDiv.addClass('month-container'); + monthDiv.data('month-id', m); + + var firstDate = new Date(this.options.startYear, m, 1); + + var table = $(document.createElement('table')); + table.addClass('month'); + + /* Month header */ + var thead = $(document.createElement('thead')); + + var titleRow = $(document.createElement('tr')); + + var titleCell = $(document.createElement('th')); + titleCell.addClass('month-title'); + titleCell.attr('colspan', this.options.displayWeekNumber ? 8 : 7); + titleCell.text(dates[this.options.language].months[m]); + + titleRow.append(titleCell); + thead.append(titleRow); + + var headerRow = $(document.createElement('tr')); + + if(this.options.displayWeekNumber) { + var weekNumberCell = $(document.createElement('th')); + weekNumberCell.addClass('week-number'); + weekNumberCell.text(dates[this.options.language].weekShort); + headerRow.append(weekNumberCell); + } + + var weekStart = this.options.weekStart ? this.options.weekStart : dates[this.options.language].weekStart; + var d = weekStart; + do + { + var headerCell = $(document.createElement('th')); + headerCell.addClass('day-header'); + headerCell.text(dates[this.options.language].daysMin[d]); + + if(this._isHidden(d)) { + headerCell.addClass('hidden'); + } + + headerRow.append(headerCell); + + d++; + if(d >= 7) + d = 0; + } + while(d != weekStart) + + thead.append(headerRow); + table.append(thead); + + /* Days */ + var currentDate = new Date(firstDate.getTime()); + var lastDate = new Date(this.options.startYear, m + 1, 0); + + while(currentDate.getDay() != weekStart) + { + currentDate.setDate(currentDate.getDate() - 1); + } + + while(currentDate <= lastDate) + { + var row = $(document.createElement('tr')); + + if(this.options.displayWeekNumber) { + var weekNumberCell = $(document.createElement('td')); + weekNumberCell.addClass('week-number'); + weekNumberCell.text(this.getWeekNumber(currentDate)); + row.append(weekNumberCell); + } + + do + { + var cell = $(document.createElement('td')); + cell.addClass('day'); + + if(this._isHidden(currentDate.getDay())) { + cell.addClass('hidden'); + } + + if(currentDate < firstDate) { + cell.addClass('old'); + } + else if(currentDate > lastDate) { + cell.addClass('new'); + } + else { + if(this._isDisabled(currentDate)) { + cell.addClass('disabled'); + } + + var cellContent = $(document.createElement('div')); + cellContent.addClass('day-content'); + cellContent.text(currentDate.getDate()); + cell.append(cellContent); + + if(this.options.customDayRenderer) { + this.options.customDayRenderer(cellContent, currentDate); + } + } + + row.append(cell); + + currentDate.setDate(currentDate.getDate() + 1); + } + while(currentDate.getDay() != weekStart) + + table.append(row); + } + + monthDiv.append(table); + + monthsDiv.append(monthDiv); + } + + this.element.append(monthsDiv); + }, + _renderDataSource: function() { + var _this = this; + if(this.options.dataSource != null && this.options.dataSource.length > 0) { + this.element.find('.month-container').each(function() { + var month = $(this).data('month-id'); + + var firstDate = new Date(_this.options.startYear, month, 1); + var lastDate = new Date(_this.options.startYear, month + 1, 1); + + if((_this.options.minDate == null || lastDate > _this.options.minDate) && (_this.options.maxDate == null || firstDate <= _this.options.maxDate)) + { + var monthData = []; + + for(var i = 0; i < _this.options.dataSource.length; i++) { + if(!(_this.options.dataSource[i].startDate >= lastDate) || (_this.options.dataSource[i].endDate < firstDate)) { + monthData.push(_this.options.dataSource[i]); + } + } + + if(monthData.length > 0) { + $(this).find('.day-content').each(function() { + var currentDate = new Date(_this.options.startYear, month, $(this).text()); + var nextDate = new Date(_this.options.startYear, month, currentDate.getDate() + 1); + + var dayData = []; + + if((_this.options.minDate == null || currentDate >= _this.options.minDate) && (_this.options.maxDate == null || currentDate <= _this.options.maxDate)) + { + for(var i = 0; i < monthData.length; i++) { + if(monthData[i].startDate < nextDate && monthData[i].endDate >= currentDate) { + dayData.push(monthData[i]); + } + } + + if(dayData.length > 0 && (_this.options.displayDisabledDataSource || !_this._isDisabled(currentDate))) + { + _this._renderDataSourceDay($(this), currentDate, dayData); + } + } + }); + } + } + }); + } + }, + _renderDataSourceDay: function(elt, currentDate, events) { + switch(this.options.style) + { + case 'border': + var weight = 0; + + if(events.length == 1) { + weight = 4; + } + else if(events.length <= 3) { + weight = 2; + } + else { + elt.parent().css('box-shadow', 'inset 0 -4px 0 0 black'); + } + + if(weight > 0) + { + var boxShadow = ''; + + for (var i = 0; i < events.length; i++) + { + if(boxShadow != '') { + boxShadow += ","; + } + + boxShadow += 'inset 0 -' + (parseInt(i) + 1) * weight + 'px 0 0 ' + events[i].color; + } + + elt.parent().css('box-shadow', boxShadow); + } + break; + + case 'background': + elt.parent().css('background-color', events[events.length - 1].color); + + var currentTime = currentDate.getTime(); + + if(events[events.length - 1].startDate.getTime() == currentTime) + { + elt.parent().addClass('day-start'); + + if(events[events.length - 1].startHalfDay || this.options.alwaysHalfDay) { + elt.parent().addClass('day-half'); + + // Find color for other half + var otherColor = 'transparent'; + for(var i = events.length - 2; i >= 0; i--) { + if(events[i].startDate.getTime() != currentTime || (!events[i].startHalfDay && !this.options.alwaysHalfDay)) { + otherColor = events[i].color; + break; + } + } + + elt.parent().css('background', 'linear-gradient(-45deg, ' + events[events.length - 1].color + ', ' + events[events.length - 1].color + ' 49%, ' + otherColor + ' 51%, ' + otherColor + ')'); + } + else if(this.options.roundRangeLimits) { + elt.parent().addClass('round-left'); + } + } + else if(events[events.length - 1].endDate.getTime() == currentTime) + { + elt.parent().addClass('day-end'); + + if(events[events.length - 1].endHalfDay || this.options.alwaysHalfDay) { + elt.parent().addClass('day-half'); + + // Find color for other half + var otherColor = 'transparent'; + for(var i = events.length - 2; i >= 0; i--) { + if(events[i].endDate.getTime() != currentTime || (!events[i].endHalfDay && !this.options.alwaysHalfDay)) { + otherColor = events[i].color; + break; + } + } + + elt.parent().css('background', 'linear-gradient(135deg, ' + events[events.length - 1].color + ', ' + events[events.length - 1].color + ' 49%, ' + otherColor + ' 51%, ' + otherColor + ')'); + } + else if(this.options.roundRangeLimits) { + elt.parent().addClass('round-right'); + } + } + break; + + case 'custom': + if(this.options.customDataSourceRenderer) { + this.options.customDataSourceRenderer.call(this, elt, currentDate, events); + } + break; + } + }, + _applyEvents: function () { + var _this = this; + + /* Header buttons */ + this.element.find('.year-neighbor, .year-neighbor2').click(function() { + if(!$(this).hasClass('disabled')) { + _this.setYear(parseInt($(this).text())); + } + }); + + this.element.find('.calendar-header .prev').click(function() { + if(!$(this).hasClass('disabled')) { + _this.element.find('.months-container').animate({'margin-left':'100%'},100, function() { + _this.element.find('.months-container').css('visibility', 'hidden'); + _this.element.find('.months-container').css('margin-left', '0'); + + setTimeout(function() { + _this.setYear(_this.options.startYear - 1); + }, 50); + }); + } + }); + + this.element.find('.calendar-header .next').click(function() { + if(!$(this).hasClass('disabled')) { + _this.element.find('.months-container').animate({'margin-left':'-100%'},100, function() { + _this.element.find('.months-container').css('visibility', 'hidden'); + _this.element.find('.months-container').css('margin-left', '0'); + + setTimeout(function() { + _this.setYear(_this.options.startYear + 1); + }, 50); + }); + } + }); + + var cells = this.element.find('.day:not(.old, .new, .disabled)'); + + /* Click on date */ + cells.click(function(e) { + e.stopPropagation(); + var date = _this._getDate($(this)); + _this._triggerEvent('clickDay', { + element: $(this), + which: e.which, + date: date, + events: _this.getEvents(date) + }); + }); + + /* Click right on date */ + + cells.bind('contextmenu', function(e) { + if(_this.options.enableContextMenu) + { + e.preventDefault(); + if(_this.options.contextMenuItems.length > 0) + { + _this._openContextMenu($(this)); + } + } + + var date = _this._getDate($(this)); + _this._triggerEvent('dayContextMenu', { + element: $(this), + date: date, + events: _this.getEvents(date) + }); + }); + + /* Range selection */ + if(this.options.enableRangeSelection) { + cells.mousedown(function (e) { + if(e.which == 1) { + var currentDate = _this._getDate($(this)); + + if(_this.options.allowOverlap || _this.getEvents(currentDate).length == 0) + { + _this._mouseDown = true; + _this._rangeStart = _this._rangeEnd = currentDate; + _this._refreshRange(); + } + } + }); + + cells.mouseenter(function (e) { + if (_this._mouseDown) { + var currentDate = _this._getDate($(this)); + + if(!_this.options.allowOverlap) + { + var newDate = new Date(_this._rangeStart.getTime()); + + if(newDate < currentDate) { + var nextDate = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate() + 1); + while(newDate < currentDate) { + if(_this.getEvents(nextDate).length > 0) + { + break; + } + + newDate.setDate(newDate.getDate() + 1); + nextDate.setDate(nextDate.getDate() + 1); + } + } + else { + var nextDate = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate() - 1); + while(newDate > currentDate) { + if(_this.getEvents(nextDate).length > 0) + { + break; + } + + newDate.setDate(newDate.getDate() - 1); + nextDate.setDate(nextDate.getDate() - 1); + } + } + + currentDate = newDate; + } + + var oldValue = _this._rangeEnd; + _this._rangeEnd = currentDate; + + if (oldValue.getTime() != _this._rangeEnd.getTime()) { + _this._refreshRange(); + } + } + }); + + $(window).mouseup(function (e) { + if (_this._mouseDown) { + _this._mouseDown = false; + _this._refreshRange(); + + var minDate = _this._rangeStart < _this._rangeEnd ? _this._rangeStart : _this._rangeEnd; + var maxDate = _this._rangeEnd > _this._rangeStart ? _this._rangeEnd : _this._rangeStart; + + _this._triggerEvent('selectRange', { + startDate: minDate, + endDate: maxDate, + events: _this.getEventsOnRange(minDate, new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate() + 1)) + }); + } + }); + } + + /* Hover date */ + cells.mouseenter(function(e) { + if(!_this._mouseDown) + { + var date = _this._getDate($(this)); + _this._triggerEvent('mouseOnDay', { + element: $(this), + date: date, + events: _this.getEvents(date) + }); + } + }); + + cells.mouseleave(function(e) { + var date = _this._getDate($(this)); + _this._triggerEvent('mouseOutDay', { + element: $(this), + date: date, + events: _this.getEvents(date) + }); + }); + + /* Responsive management */ + + setInterval(function() { + var calendarSize = $(_this.element).width(); + var monthSize = $(_this.element).find('.month').first().width() + 10; + var monthContainerClass = 'month-container'; + + if(monthSize * 6 < calendarSize) { + monthContainerClass += ' col-xs-2'; + } + else if(monthSize * 4 < calendarSize) { + monthContainerClass += ' col-xs-3'; + } + else if(monthSize * 3 < calendarSize) { + monthContainerClass += ' col-xs-4'; + } + else if(monthSize * 2 < calendarSize) { + monthContainerClass += ' col-xs-6'; + } + else { + monthContainerClass += ' col-xs-12'; + } + + $(_this.element).find('.month-container').attr('class', monthContainerClass); + }, 300); + }, + _refreshRange: function () { + var _this = this; + + this.element.find('td.day.range').removeClass('range') + this.element.find('td.day.range-start').removeClass('range-start'); + this.element.find('td.day.range-end').removeClass('range-end'); + + if (this._mouseDown) { + var beforeRange = true; + var afterRange = false; + var minDate = _this._rangeStart < _this._rangeEnd ? _this._rangeStart : _this._rangeEnd; + var maxDate = _this._rangeEnd > _this._rangeStart ? _this._rangeEnd : _this._rangeStart; + + this.element.find('.month-container').each(function () { + var monthId = $(this).data('month-id'); + if (minDate.getMonth() <= monthId && maxDate.getMonth() >= monthId) { + $(this).find('td.day:not(.old, .new)').each(function () { + var date = _this._getDate($(this)); + if (date >= minDate && date <= maxDate) { + $(this).addClass('range'); + + if (date.getTime() == minDate.getTime()) { + $(this).addClass('range-start'); + } + + if (date.getTime() == maxDate.getTime()) { + $(this).addClass('range-end'); + } + } + }); + } + }); + } + }, + _openContextMenu: function(elt) { + var contextMenu = $('.calendar-context-menu'); + + if(contextMenu.length > 0) { + contextMenu.hide(); + contextMenu.empty(); + } + else { + contextMenu = $(document.createElement('div')); + contextMenu.addClass('calendar-context-menu'); + $('body').append(contextMenu); + } + + var date = this._getDate(elt); + var events = this.getEvents(date); + + for(var i = 0; i < events.length; i++) { + var eventItem = $(document.createElement('div')); + eventItem.addClass('item'); + eventItem.css('border-left', '4px solid ' + events[i].color); + + var eventItemContent = $(document.createElement('div')); + eventItemContent.addClass('content'); + eventItemContent.text(events[i].name); + + eventItem.append(eventItemContent); + + var icon = $(document.createElement('span')); + icon.addClass('glyphicon glyphicon-chevron-right'); + + eventItem.append(icon); + + this._renderContextMenuItems(eventItem, this.options.contextMenuItems, events[i]); + + contextMenu.append(eventItem); + } + + if(contextMenu.children().length > 0) + { + contextMenu.css('left', elt.offset().left + 25 + 'px'); + contextMenu.css('top', elt.offset().top + 25 + 'px'); + contextMenu.show(); + + $(window).one('mouseup', function() { + contextMenu.hide(); + }); + } + }, + _renderContextMenuItems: function(parent, items, evt) { + var subMenu = $(document.createElement('div')); + subMenu.addClass('submenu'); + + for(var i = 0; i < items.length; i++) { + if(!items[i].visible || items[i].visible(evt)) { + var menuItem = $(document.createElement('div')); + menuItem.addClass('item'); + + var menuItemContent = $(document.createElement('div')); + menuItemContent.addClass('content'); + menuItemContent.text(items[i].text); + + menuItem.append(menuItemContent); + + if(items[i].click) { + (function(index) { + menuItem.click(function() { + items[index].click(evt); + }); + })(i); + } + + var icon = $(document.createElement('span')); + icon.addClass('glyphicon glyphicon-chevron-right'); + + menuItem.append(icon); + + if(items[i].items && items[i].items.length > 0) { + this._renderContextMenuItems(menuItem, items[i].items, evt); + } + + subMenu.append(menuItem); + } + } + + if(subMenu.children().length > 0) + { + parent.append(subMenu); + } + }, + _getColor: function(colorString) { + var div = $('
'); + div.css('color', colorString); + + }, + _getDate: function(elt) { + var day = elt.children('.day-content').text(); + var month = elt.closest('.month-container').data('month-id'); + var year = this.options.startYear; + + return new Date(year, month, day); + }, + _triggerEvent: function(eventName, parameters) { + var event = $.Event(eventName); + + for(var i in parameters) { + event[i] = parameters[i]; + } + + this.element.trigger(event); + + return event; + }, + _isDisabled: function(date) { + if((this.options.minDate != null && date < this.options.minDate) || (this.options.maxDate != null && date > this.options.maxDate)) + { + return true; + } + + if(this.options.disabledWeekDays.length > 0) { + for(var d = 0; d < this.options.disabledWeekDays.length; d++){ + if(date.getDay() == this.options.disabledWeekDays[d]) { + return true; + } + } + } + + if(this.options.disabledDays.length > 0) { + for(var d = 0; d < this.options.disabledDays.length; d++){ + if(date.getTime() == this.options.disabledDays[d].getTime()) { + return true; + } + } + } + + return false; + }, + _isHidden: function(day) { + if(this.options.hiddenWeekDays.length > 0) { + for(var d = 0; d < this.options.hiddenWeekDays.length; d++) { + if(day == this.options.hiddenWeekDays[d]) { + return true; + } + } + } + + return false; + }, + getWeekNumber: function(date) { + var tempDate = new Date(date.getTime()); + tempDate.setHours(0, 0, 0, 0); + tempDate.setDate(tempDate.getDate() + 3 - (tempDate.getDay() + 6) % 7); + var week1 = new Date(tempDate.getFullYear(), 0, 4); + return 1 + Math.round(((tempDate.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7); + }, + getEvents: function(date) { + return this.getEventsOnRange(date, new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)); + }, + getEventsOnRange: function(startDate, endDate) { + var events = []; + + if(this.options.dataSource && startDate && endDate) { + for(var i = 0; i < this.options.dataSource.length; i++) { + if(this.options.dataSource[i].startDate < endDate && this.options.dataSource[i].endDate >= startDate) { + events.push(this.options.dataSource[i]); + } + } + } + + return events; + }, + getYear: function() { + return this.options.startYear; + }, + setYear: function(year) { + var parsedYear = parseInt(year); + if(!isNaN(parsedYear)) { + this.options.startYear = parsedYear; + + this.element.empty(); + + if(this.options.displayHeader) { + this._renderHeader(); + } + + var eventResult = this._triggerEvent('yearChanged', { currentYear: this.options.startYear, preventRendering: false }); + + if(!eventResult.preventRendering) { + this.render(); + } + } + }, + getMinDate: function() { + return this.options.minDate; + }, + setMinDate: function(date, preventRendering) { + if(date instanceof Date) { + this.options.minDate = date; + + if(!preventRendering) { + this.render(); + } + } + }, + getMaxDate: function() { + return this.options.maxDate; + }, + setMaxDate: function(date, preventRendering) { + if(date instanceof Date) { + this.options.maxDate = date; + + if(!preventRendering) { + this.render(); + } + } + }, + getStyle: function() { + return this.options.style; + }, + setStyle: function(style, preventRendering) { + this.options.style = style == 'background' || style == 'border' || style == 'custom' ? style : 'border'; + + if(!preventRendering) { + this.render(); + } + }, + getAllowOverlap: function() { + return this.options.allowOverlap; + }, + setAllowOverlap: function(allowOverlap) { + this.options.allowOverlap = allowOverlap; + }, + getDisplayWeekNumber: function() { + return this.options.displayWeekNumber; + }, + setDisplayWeekNumber: function(displayWeekNumber, preventRendering) { + this.options.displayWeekNumber = displayWeekNumber; + + if(!preventRendering) { + this.render(); + } + }, + getDisplayHeader: function() { + return this.options.displayHeader; + }, + setDisplayHeader: function(displayHeader, preventRendering) { + this.options.displayHeader = displayHeader; + + if(!preventRendering) { + this.render(); + } + }, + getDisplayDisabledDataSource: function() { + return this.options.displayDisabledDataSource; + }, + setDisplayDisabledDataSource: function(displayDisabledDataSource, preventRendering) { + this.options.displayDisabledDataSource = displayDisabledDataSource; + + if(!preventRendering) { + this.render(); + } + }, + getAlwaysHalfDay: function() { + return this.options.alwaysHalfDay; + }, + setAlwaysHalfDay: function(alwaysHalfDay, preventRendering) { + this.options.alwaysHalfDay = alwaysHalfDay; + + if(!preventRendering) { + this.render(); + } + }, + getEnableRangeSelection: function() { + return this.options.enableRangeSelection; + }, + setEnableRangeSelection: function(enableRangeSelection, preventRendering) { + this.options.enableRangeSelection = enableRangeSelection; + + if(!preventRendering) { + this.render(); + } + }, + getDisabledDays: function() { + return this.options.disabledDays; + }, + setDisabledDays: function(disabledDays, preventRendering) { + this.options.disabledDays = disabledDays instanceof Array ? disabledDays : []; + + if(!preventRendering) { + this.render(); + } + }, + getDisabledWeekDays: function() { + return this.options.disabledWeekDays; + }, + setDisabledWeekDays: function(disabledWeekDays, preventRendering) { + this.options.disabledWeekDays = disabledWeekDays instanceof Array ? disabledWeekDays : []; + + if(!preventRendering) { + this.render(); + } + }, + getHiddenWeekDays: function() { + return this.options.hiddenWeekDays; + }, + setHiddenWeekDays: function(hiddenWeekDays, preventRendering) { + this.options.hiddenWeekDays = hiddenWeekDays instanceof Array ? hiddenWeekDays : []; + + if(!preventRendering) { + this.render(); + } + }, + getRoundRangeLimits: function() { + return this.options.roundRangeLimits; + }, + setRoundRangeLimits: function(roundRangeLimits, preventRendering) { + this.options.roundRangeLimits = roundRangeLimits; + + if(!preventRendering) { + this.render(); + } + }, + getEnableContextMenu: function() { + return this.options.enableContextMenu; + }, + setEnableContextMenu: function(enableContextMenu, preventRendering) { + this.options.enableContextMenu = enableContextMenu; + + if(!preventRendering) { + this.render(); + } + }, + getContextMenuItems: function() { + return this.options.contextMenuItems; + }, + setContextMenuItems: function(contextMenuItems, preventRendering) { + this.options.contextMenuItems = contextMenuItems instanceof Array ? contextMenuItems : []; + + if(!preventRendering) { + this.render(); + } + }, + getCustomDayRenderer: function() { + return this.options.customDayRenderer; + }, + setCustomDayRenderer: function(customDayRenderer, preventRendering) { + this.options.customDayRenderer = $.isFunction(customDayRenderer) ? customDayRenderer : null; + + if(!preventRendering) { + this.render(); + } + }, + getCustomDataSourceRenderer: function() { + return this.options.customDataSourceRenderer; + }, + setCustomDataSourceRenderer: function(customDataSourceRenderer, preventRendering) { + this.options.customDataSourceRenderer = $.isFunction(customDataSourceRenderer) ? customDataSourceRenderer : null; + + if(!preventRendering) { + this.render(); + } + }, + getLanguage: function() { + return this.options.language; + }, + setLanguage: function(language, preventRendering) { + if(language != null && dates[language] != null) { + this.options.language = language; + + if(!preventRendering) { + this.render(); + } + } + }, + getDataSource: function() { + return this.options.dataSource; + }, + setDataSource: function(dataSource, preventRendering) { + this.options.dataSource = dataSource instanceof Array ? dataSource : []; + this._initializeDatasourceColors(); + + if(!preventRendering) { + this.render(); + } + }, + getWeekStart: function() { + return this.options.weekStart ? this.options.weekStart : dates[this.options.language].weekStart; + }, + setWeekStart: function(weekStart, preventRendering) { + this.options.weekStart = !isNaN(parseInt(weekStart)) ? parseInt(weekStart) : null; + + if(!preventRendering) { + this.render(); + } + }, + addEvent: function(evt, preventRendering) { + this.options.dataSource.push(evt); + + if(!preventRendering) { + this.render(); + } + } + } + + $.fn.calendar = function (options) { + var calendar = new Calendar($(this) ,options); + $(this).data('calendar', calendar); + return calendar; + } + + /* Events binding management */ + $.fn.yearChanged = function(fct) { $(this).bind('yearChanged', fct); } + $.fn.renderEnd = function(fct) { $(this).bind('renderEnd', fct); } + $.fn.clickDay = function(fct) { $(this).bind('clickDay', fct); } + $.fn.dayContextMenu = function(fct) { $(this).bind('dayContextMenu', fct); } + $.fn.selectRange = function(fct) { $(this).bind('selectRange', fct); } + $.fn.mouseOnDay = function(fct) { $(this).bind('mouseOnDay', fct); } + $.fn.mouseOutDay = function(fct) { $(this).bind('mouseOutDay', fct); } + + var dates = $.fn.calendar.dates = { + en: { + days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], + daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], + months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + weekShort: 'W', + weekStart:0 + } + }; + + var colors = $.fn.calendar.colors = ['#2C8FC9', '#9CB703', '#F5BB00', '#FF4A32', '#B56CE2', '#45A597']; + + $(function(){ + $('[data-provide="calendar"]').each(function() { + $(this).calendar(); + }); + }); + }(window.jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/bootstrap-year-calendar.min.js b/swh/web/static/js/bootstrap-year-calendar/bootstrap-year-calendar.min.js new file mode 100644 index 00000000..09562ec2 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/bootstrap-year-calendar.min.js @@ -0,0 +1,7 @@ +/* + * Bootstrap year calendar v1.1.0 + * Created by Paul David-Sivelle + * Licensed under the Apache License, Version 2.0 + */ + +!function(a){var b=function(a,b){this.element=a,this.element.addClass("calendar"),this._initializeEvents(b),this._initializeOptions(b),this.setYear(this.options.startYear)};b.prototype={constructor:b,_initializeOptions:function(b){null==b&&(b=[]),this.options={startYear:isNaN(parseInt(b.startYear))?(new Date).getFullYear():parseInt(b.startYear),minDate:b.minDate instanceof Date?b.minDate:null,maxDate:b.maxDate instanceof Date?b.maxDate:null,language:null!=b.language&&null!=c[b.language]?b.language:"en",allowOverlap:null==b.allowOverlap||b.allowOverlap,displayWeekNumber:null!=b.displayWeekNumber&&b.displayWeekNumber,displayDisabledDataSource:null!=b.displayDisabledDataSource&&b.displayDisabledDataSource,displayHeader:null==b.displayHeader||b.displayHeader,alwaysHalfDay:null!=b.alwaysHalfDay&&b.alwaysHalfDay,enableRangeSelection:null!=b.enableRangeSelection&&b.enableRangeSelection,disabledDays:b.disabledDays instanceof Array?b.disabledDays:[],disabledWeekDays:b.disabledWeekDays instanceof Array?b.disabledWeekDays:[],hiddenWeekDays:b.hiddenWeekDays instanceof Array?b.hiddenWeekDays:[],roundRangeLimits:null!=b.roundRangeLimits&&b.roundRangeLimits,dataSource:b.dataSource instanceof Array?b.dataSource:[],style:"background"==b.style||"border"==b.style||"custom"==b.style?b.style:"border",enableContextMenu:null!=b.enableContextMenu&&b.enableContextMenu,contextMenuItems:b.contextMenuItems instanceof Array?b.contextMenuItems:[],customDayRenderer:a.isFunction(b.customDayRenderer)?b.customDayRenderer:null,customDataSourceRenderer:a.isFunction(b.customDataSourceRenderer)?b.customDataSourceRenderer:null,weekStart:isNaN(parseInt(b.weekStart))?null:parseInt(b.weekStart)},this._initializeDatasourceColors()},_initializeEvents:function(a){null==a&&(a=[]),a.yearChanged&&this.element.bind("yearChanged",a.yearChanged),a.renderEnd&&this.element.bind("renderEnd",a.renderEnd),a.clickDay&&this.element.bind("clickDay",a.clickDay),a.dayContextMenu&&this.element.bind("dayContextMenu",a.dayContextMenu),a.selectRange&&this.element.bind("selectRange",a.selectRange),a.mouseOnDay&&this.element.bind("mouseOnDay",a.mouseOnDay),a.mouseOutDay&&this.element.bind("mouseOutDay",a.mouseOutDay)},_initializeDatasourceColors:function(){for(var a=0;anew Date(this.options.startYear-1,11,31)&&d.addClass("disabled");var e=a(document.createElement("span"));e.addClass("glyphicon glyphicon-chevron-left"),d.append(e),c.append(d);var f=a(document.createElement("th"));f.addClass("year-title year-neighbor2 hidden-sm hidden-xs"),f.text(this.options.startYear-2),null!=this.options.minDate&&this.options.minDate>new Date(this.options.startYear-2,11,31)&&f.addClass("disabled"),c.append(f);var g=a(document.createElement("th"));g.addClass("year-title year-neighbor hidden-xs"),g.text(this.options.startYear-1),null!=this.options.minDate&&this.options.minDate>new Date(this.options.startYear-1,11,31)&&g.addClass("disabled"),c.append(g);var h=a(document.createElement("th"));h.addClass("year-title"),h.text(this.options.startYear),c.append(h);var i=a(document.createElement("th"));i.addClass("year-title year-neighbor hidden-xs"),i.text(this.options.startYear+1),null!=this.options.maxDate&&this.options.maxDate=7&&(n=0)}while(n!=m);h.append(k),g.append(h);for(var p=new Date(f.getTime()),q=new Date(this.options.startYear,d+1,0);p.getDay()!=m;)p.setDate(p.getDate()-1);for(;p<=q;){var r=a(document.createElement("tr"));if(this.options.displayWeekNumber){var l=a(document.createElement("td"));l.addClass("week-number"),l.text(this.getWeekNumber(p)),r.append(l)}do{var s=a(document.createElement("td"));if(s.addClass("day"),this._isHidden(p.getDay())&&s.addClass("hidden"),pq)s.addClass("new");else{this._isDisabled(p)&&s.addClass("disabled");var t=a(document.createElement("div"));t.addClass("day-content"),t.text(p.getDate()),s.append(t),this.options.customDayRenderer&&this.options.customDayRenderer(t,p)}r.append(s),p.setDate(p.getDate()+1)}while(p.getDay()!=m);g.append(r)}e.append(g),b.append(e)}this.element.append(b)},_renderDataSource:function(){var b=this;null!=this.options.dataSource&&this.options.dataSource.length>0&&this.element.find(".month-container").each(function(){var c=a(this).data("month-id"),d=new Date(b.options.startYear,c,1),e=new Date(b.options.startYear,c+1,1);if((null==b.options.minDate||e>b.options.minDate)&&(null==b.options.maxDate||d<=b.options.maxDate)){for(var f=[],g=0;g=e&&!(b.options.dataSource[g].endDate0&&a(this).find(".day-content").each(function(){var d=new Date(b.options.startYear,c,a(this).text()),e=new Date(b.options.startYear,c,d.getDate()+1),g=[];if((null==b.options.minDate||d>=b.options.minDate)&&(null==b.options.maxDate||d<=b.options.maxDate)){for(var h=0;h=d&&g.push(f[h]);g.length>0&&(b.options.displayDisabledDataSource||!b._isDisabled(d))&&b._renderDataSourceDay(a(this),d,g)}})}})},_renderDataSourceDay:function(a,b,c){switch(this.options.style){case"border":var d=0;if(1==c.length?d=4:c.length<=3?d=2:a.parent().css("box-shadow","inset 0 -4px 0 0 black"),d>0){for(var e="",f=0;f=0;f--)if(c[f].startDate.getTime()!=g||!c[f].startHalfDay&&!this.options.alwaysHalfDay){h=c[f].color;break}a.parent().css("background","linear-gradient(-45deg, "+c[c.length-1].color+", "+c[c.length-1].color+" 49%, "+h+" 51%, "+h+")")}else this.options.roundRangeLimits&&a.parent().addClass("round-left");else if(c[c.length-1].endDate.getTime()==g)if(a.parent().addClass("day-end"),c[c.length-1].endHalfDay||this.options.alwaysHalfDay){a.parent().addClass("day-half");for(var h="transparent",f=c.length-2;f>=0;f--)if(c[f].endDate.getTime()!=g||!c[f].endHalfDay&&!this.options.alwaysHalfDay){h=c[f].color;break}a.parent().css("background","linear-gradient(135deg, "+c[c.length-1].color+", "+c[c.length-1].color+" 49%, "+h+" 51%, "+h+")")}else this.options.roundRangeLimits&&a.parent().addClass("round-right");break;case"custom":this.options.customDataSourceRenderer&&this.options.customDataSourceRenderer.call(this,a,b,c)}},_applyEvents:function(){var b=this;this.element.find(".year-neighbor, .year-neighbor2").click(function(){a(this).hasClass("disabled")||b.setYear(parseInt(a(this).text()))}),this.element.find(".calendar-header .prev").click(function(){a(this).hasClass("disabled")||b.element.find(".months-container").animate({"margin-left":"100%"},100,function(){b.element.find(".months-container").css("visibility","hidden"),b.element.find(".months-container").css("margin-left","0"),setTimeout(function(){b.setYear(b.options.startYear-1)},50)})}),this.element.find(".calendar-header .next").click(function(){a(this).hasClass("disabled")||b.element.find(".months-container").animate({"margin-left":"-100%"},100,function(){b.element.find(".months-container").css("visibility","hidden"),b.element.find(".months-container").css("margin-left","0"),setTimeout(function(){b.setYear(b.options.startYear+1)},50)})});var c=this.element.find(".day:not(.old, .new, .disabled)");c.click(function(c){c.stopPropagation();var d=b._getDate(a(this));b._triggerEvent("clickDay",{element:a(this),which:c.which,date:d,events:b.getEvents(d)})}),c.bind("contextmenu",function(c){b.options.enableContextMenu&&(c.preventDefault(),b.options.contextMenuItems.length>0&&b._openContextMenu(a(this)));var d=b._getDate(a(this));b._triggerEvent("dayContextMenu",{element:a(this),date:d,events:b.getEvents(d)})}),this.options.enableRangeSelection&&(c.mousedown(function(c){if(1==c.which){var d=b._getDate(a(this));(b.options.allowOverlap||0==b.getEvents(d).length)&&(b._mouseDown=!0,b._rangeStart=b._rangeEnd=d,b._refreshRange())}}),c.mouseenter(function(c){if(b._mouseDown){var d=b._getDate(a(this));if(!b.options.allowOverlap){var e=new Date(b._rangeStart.getTime());if(e0);)e.setDate(e.getDate()+1),f.setDate(f.getDate()+1);else for(var f=new Date(e.getFullYear(),e.getMonth(),e.getDate()-1);e>d&&!(b.getEvents(f).length>0);)e.setDate(e.getDate()-1),f.setDate(f.getDate()-1);d=e}var g=b._rangeEnd;b._rangeEnd=d,g.getTime()!=b._rangeEnd.getTime()&&b._refreshRange()}}),a(window).mouseup(function(a){if(b._mouseDown){b._mouseDown=!1,b._refreshRange();var c=b._rangeStartb._rangeStart?b._rangeEnd:b._rangeStart;b._triggerEvent("selectRange",{startDate:c,endDate:d,events:b.getEventsOnRange(c,new Date(d.getFullYear(),d.getMonth(),d.getDate()+1))})}})),c.mouseenter(function(c){if(!b._mouseDown){var d=b._getDate(a(this));b._triggerEvent("mouseOnDay",{element:a(this),date:d,events:b.getEvents(d)})}}),c.mouseleave(function(c){var d=b._getDate(a(this));b._triggerEvent("mouseOutDay",{element:a(this),date:d,events:b.getEvents(d)})}),setInterval(function(){var c=a(b.element).width(),d=a(b.element).find(".month").first().width()+10,e="month-container";e+=6*db._rangeStart?b._rangeEnd:b._rangeStart;this.element.find(".month-container").each(function(){var c=a(this).data("month-id");e.getMonth()<=c&&f.getMonth()>=c&&a(this).find("td.day:not(.old, .new)").each(function(){var c=b._getDate(a(this));c>=e&&c<=f&&(a(this).addClass("range"),c.getTime()==e.getTime()&&a(this).addClass("range-start"),c.getTime()==f.getTime()&&a(this).addClass("range-end"))})})}},_openContextMenu:function(b){var c=a(".calendar-context-menu");c.length>0?(c.hide(),c.empty()):(c=a(document.createElement("div")),c.addClass("calendar-context-menu"),a("body").append(c));for(var d=this._getDate(b),e=this.getEvents(d),f=0;f0&&(c.css("left",b.offset().left+25+"px"),c.css("top",b.offset().top+25+"px"),c.show(),a(window).one("mouseup",function(){c.hide()}))},_renderContextMenuItems:function(b,c,d){var e=a(document.createElement("div"));e.addClass("submenu");for(var f=0;f0&&this._renderContextMenuItems(g,c[f].items,d),e.append(g)}e.children().length>0&&b.append(e)},_getColor:function(b){var c=a("
");c.css("color",b)},_getDate:function(a){var b=a.children(".day-content").text(),c=a.closest(".month-container").data("month-id"),d=this.options.startYear;return new Date(d,c,b)},_triggerEvent:function(b,c){var d=a.Event(b);for(var e in c)d[e]=c[e];return this.element.trigger(d),d},_isDisabled:function(a){if(null!=this.options.minDate&&athis.options.maxDate)return!0;if(this.options.disabledWeekDays.length>0)for(var b=0;b0)for(var b=0;b0)for(var b=0;b=a&&c.push(this.options.dataSource[d]);return c},getYear:function(){return this.options.startYear},setYear:function(a){var b=parseInt(a);if(!isNaN(b)){this.options.startYear=b,this.element.empty(),this.options.displayHeader&&this._renderHeader();var c=this._triggerEvent("yearChanged",{currentYear:this.options.startYear,preventRendering:!1});c.preventRendering||this.render()}},getMinDate:function(){return this.options.minDate},setMinDate:function(a,b){a instanceof Date&&(this.options.minDate=a,b||this.render())},getMaxDate:function(){return this.options.maxDate},setMaxDate:function(a,b){a instanceof Date&&(this.options.maxDate=a,b||this.render())},getStyle:function(){return this.options.style},setStyle:function(a,b){this.options.style="background"==a||"border"==a||"custom"==a?a:"border",b||this.render()},getAllowOverlap:function(){return this.options.allowOverlap},setAllowOverlap:function(a){this.options.allowOverlap=a},getDisplayWeekNumber:function(){return this.options.displayWeekNumber},setDisplayWeekNumber:function(a,b){this.options.displayWeekNumber=a,b||this.render()},getDisplayHeader:function(){return this.options.displayHeader},setDisplayHeader:function(a,b){this.options.displayHeader=a,b||this.render()},getDisplayDisabledDataSource:function(){return this.options.displayDisabledDataSource},setDisplayDisabledDataSource:function(a,b){this.options.displayDisabledDataSource=a,b||this.render()},getAlwaysHalfDay:function(){return this.options.alwaysHalfDay},setAlwaysHalfDay:function(a,b){this.options.alwaysHalfDay=a,b||this.render()},getEnableRangeSelection:function(){return this.options.enableRangeSelection},setEnableRangeSelection:function(a,b){this.options.enableRangeSelection=a,b||this.render()},getDisabledDays:function(){return this.options.disabledDays},setDisabledDays:function(a,b){this.options.disabledDays=a instanceof Array?a:[],b||this.render()},getDisabledWeekDays:function(){return this.options.disabledWeekDays},setDisabledWeekDays:function(a,b){this.options.disabledWeekDays=a instanceof Array?a:[],b||this.render()},getHiddenWeekDays:function(){return this.options.hiddenWeekDays},setHiddenWeekDays:function(a,b){this.options.hiddenWeekDays=a instanceof Array?a:[],b||this.render()},getRoundRangeLimits:function(){return this.options.roundRangeLimits},setRoundRangeLimits:function(a,b){this.options.roundRangeLimits=a,b||this.render()},getEnableContextMenu:function(){return this.options.enableContextMenu},setEnableContextMenu:function(a,b){this.options.enableContextMenu=a,b||this.render()},getContextMenuItems:function(){return this.options.contextMenuItems},setContextMenuItems:function(a,b){this.options.contextMenuItems=a instanceof Array?a:[],b||this.render()},getCustomDayRenderer:function(){return this.options.customDayRenderer},setCustomDayRenderer:function(b,c){this.options.customDayRenderer=a.isFunction(b)?b:null,c||this.render()},getCustomDataSourceRenderer:function(){return this.options.customDataSourceRenderer},setCustomDataSourceRenderer:function(b,c){this.options.customDataSourceRenderer=a.isFunction(b)?b:null,c||this.render()},getLanguage:function(){return this.options.language},setLanguage:function(a,b){null!=a&&null!=c[a]&&(this.options.language=a,b||this.render())},getDataSource:function(){return this.options.dataSource},setDataSource:function(a,b){this.options.dataSource=a instanceof Array?a:[],this._initializeDatasourceColors(),b||this.render()},getWeekStart:function(){return this.options.weekStart?this.options.weekStart:c[this.options.language].weekStart},setWeekStart:function(a,b){this.options.weekStart=isNaN(parseInt(a))?null:parseInt(a),b||this.render()},addEvent:function(a,b){this.options.dataSource.push(a),b||this.render()}},a.fn.calendar=function(c){var d=new b(a(this),c);return a(this).data("calendar",d),d},a.fn.yearChanged=function(b){a(this).bind("yearChanged",b)},a.fn.renderEnd=function(b){a(this).bind("renderEnd",b)},a.fn.clickDay=function(b){a(this).bind("clickDay",b)},a.fn.dayContextMenu=function(b){a(this).bind("dayContextMenu",b)},a.fn.selectRange=function(b){a(this).bind("selectRange",b)},a.fn.mouseOnDay=function(b){a(this).bind("mouseOnDay",b)},a.fn.mouseOutDay=function(b){a(this).bind("mouseOutDay",b)};var c=a.fn.calendar.dates={en:{days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat","Sun"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa","Su"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],weekShort:"W",weekStart:0}},d=a.fn.calendar.colors=["#2C8FC9","#9CB703","#F5BB00","#FF4A32","#B56CE2","#45A597"];a(function(){a('[data-provide="calendar"]').each(function(){a(this).calendar()})})}(window.jQuery); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ca.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ca.js new file mode 100644 index 00000000..c32b8be4 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ca.js @@ -0,0 +1,19 @@ +/** + * Catalan translation for bootstrap-year-calendar + * David Ramirez + * Based on + * Catalan translation for bootstrap-datepicker + * J. Garcia + */ + +;(function($){ + $.fn.calendar.dates['ca'] = { + days: ["Diumenge", "Dilluns", "Dimarts", "Dimecres", "Dijous", "Divendres", "Dissabte"], + daysShort: ["Diu", "Dill", "Dim", "Dmc", "Dij", "Div", "Dis"], + daysMin: ["Dg", "Dl", "Dt", "Dc", "Dj", "Dv", "Ds"], + months: ["Gener", "Febrer", "Març", "Abril", "Maig", "Juny", "Juliol", "Agost", "Setembre", "Octubre", "Novembre", "Desembre"], + monthsShort: ["Gen", "Feb", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Oct", "Nov", "Dec"], + weekShort: 'S', + weekStart: 1 + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.cs.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.cs.js new file mode 100644 index 00000000..46697fbd --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.cs.js @@ -0,0 +1,20 @@ +/** + * Czech translation for bootstrap-year-calendar + * @ptica + * Based on + * German translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * and moment.js locale configuration by author : petrbela : https://github.com/petrbela + */ + +;(function($){ + $.fn.calendar.dates['cs'] = { + days: 'nedÄ›le_pondÄ›lí_úterý_stÅ™eda_Ätvrtek_pátek_sobota'.split('_'), + daysShort: 'ne_po_út_st_Ät_pá_so'.split('_'), + daysMin: 'ne_po_út_st_Ät_pá_so'.split('_'), + months: 'leden_únor_bÅ™ezen_duben_kvÄ›ten_Äerven_Äervenec_srpen_září_říjen_listopad_prosinec'.split('_'), + monthsShort: 'led_úno_bÅ™e_dub_kvÄ›_Ävn_Ävc_srp_zář_říj_lis_pro'.split('_'), + weekShort: '', + weekStart: 1 + }; +}(jQuery)); diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.de.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.de.js new file mode 100644 index 00000000..9f6a5890 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.de.js @@ -0,0 +1,19 @@ +/** + * German translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * German translation for bootstrap-datepicker + * Sam Zurcher + */ + +;(function($){ + $.fn.calendar.dates['de'] = { + days: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"], + daysShort: ["Son", "Mon", "Die", "Mit", "Don", "Fre", "Sam"], + daysMin: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"], + months: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], + monthsShort: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"], + weekShort: 'W', + weekStart: 1 + }; +}(jQuery)); diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.es.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.es.js new file mode 100644 index 00000000..d578d364 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.es.js @@ -0,0 +1,19 @@ +/** + * Spanish translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * Spanish translation for bootstrap-datepicker + * Bruno Bonamin + */ + +;(function($){ + $.fn.calendar.dates['es'] = { + days: ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"], + daysShort: ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb"], + daysMin: ["Do", "Lu", "Ma", "Mi", "Ju", "Vi", "Sa"], + months: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"], + monthsShort: ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"], + weekShort: 'S', + weekStart: 1 + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.fr.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.fr.js new file mode 100644 index 00000000..52d27f4a --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.fr.js @@ -0,0 +1,19 @@ +/** + * French translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * French translation for bootstrap-datepicker + * Nico Mollet + */ + +;(function($){ + $.fn.calendar.dates['fr'] = { + days: ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"], + daysShort: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"], + daysMin: ["D", "L", "Ma", "Me", "J", "V", "S", "D"], + months: ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"], + monthsShort: ["Jan", "Fév", "Mar", "Avr", "Mai", "Jui", "Jul", "Aou", "Sep", "Oct", "Nov", "Déc"], + weekShort:'S', + weekStart: 1 + }; +}(jQuery)); diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.hr.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.hr.js new file mode 100644 index 00000000..3a476352 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.hr.js @@ -0,0 +1,16 @@ +/** + * Croatian translation for bootstrap-year-calendar + * Davor DoÅ¡lnec + */ + +;(function($){ + $.fn.calendar.dates['hr'] = { + days: ["Nedjelja", "Ponedjeljak", "Utorak", "Srijeda", "ÄŒetvrtak", "Petak", "Subota"], + daysShort: ["Ned", "Pon", "Uto", "Sri", "ÄŒet", "Pet", "Sub"], + daysMin: ["Ne", "Po", "Ut", "Sr", "ÄŒe", "Pe", "Su"], + months: ["SijeÄanj", "VeljaÄa", "Ožujak", "Travanj", "Svibanj", "Lipanj", "Srpanj", "Kolovoz", "Rujan", "Listopad", "Studeni", "Prosinac"], + monthsShort: ["Sij", "Vel", "Ožu", "Tra", "Svi", "Lip", "Srp", "Kol", "Ruj", "Lis", "Stu", "Pro"], + weekShort: 'T', + weekStart: 1 + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.id.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.id.js new file mode 100644 index 00000000..ba0d253e --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.id.js @@ -0,0 +1,19 @@ +/** + * Indonesian translation for bootstrap-year-calendar + * Kiki Ldc + * Based on + * Indonesian translation for bootstrap-datepicker + * Kiki Ldc <7x24th@gmail.com> + */ + + ;(Function($) { + $.fn.calendar.dates ['id'] = { + days: ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"], + daysShort: ["Ming", "Sen", "Sel", "Rab", "kam", "Jum", "Sab"], + daysMin: ["Mg", "Sn", "Sl", "Rb", "Km", "Jm", "Sb"], + months: ["Januari", "Februari", "Maret", "April", "Mai", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember" ], + monthsShort: ["Jan", "Feb", "Mar", "Apr", "Mai", "Jun", "Jul", "Agt", "Sep", "Okt", "Nov", "Des"], + weekShort: 'W' + weekStart: 1 + }; + }(JQuery)); diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.is.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.is.js new file mode 100644 index 00000000..ff032634 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.is.js @@ -0,0 +1,19 @@ +/** + * German translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * German translation for bootstrap-datepicker + * Knútur Óli Magnússon + */ + +;(function($){ + $.fn.calendar.dates['is'] = { + days: ["Sunnudagur", "Mánudagur", "Þriðjudagur", "Miðvikudagur", "Fimmtudagur", "Föstudagur", "Laugardagur"], + daysShort: ["Sun", "Mán", "Þri", "Mið", "Fim", "Fös", "Lau"], + daysMin: ["Su", "Má", "Þr", "Mi", "Fi", "Fö", "La"], + months: ["Janúar", "Febrúar", "Mars", "Apríl", "Maí", "Júní", "Júlí", "Ãgúst", "September", "Október", "Nóvember", "Desember"], + monthsShort: ["Jan", "Feb", "Mar", "Apr", "Maí", "Jún", "Júl", "Agú", "Sep", "Okt", "Nóv", "Des"], + weekShort: 'W', + weekStart: 1 + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.it.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.it.js new file mode 100644 index 00000000..00ce229d --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.it.js @@ -0,0 +1,19 @@ +/** + * Italian translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * Italian translation for bootstrap-datepicker + * Enrico Rubboli + */ + +;(function($){ + $.fn.calendar.dates['it'] = { + days: ["Domenica", "Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato"], + daysShort: ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"], + daysMin: ["Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa"], + months: ["Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"], + monthsShort: ["Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic"], + weekShort: 'S', + weekStart: 1, + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ja.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ja.js new file mode 100644 index 00000000..c9f90c8b --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ja.js @@ -0,0 +1,19 @@ +/** + * Japanese translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * Japanese translation for bootstrap-datepicker + * Norio Suzuki + */ + +;(function($){ + $.fn.calendar.dates['ja'] = { + days: ["日曜", "月曜", "ç«æ›œ", "水曜", "木曜", "金曜", "土曜"], + daysShort: ["æ—¥", "月", "ç«", "æ°´", "木", "金", "土"], + daysMin: ["æ—¥", "月", "ç«", "æ°´", "木", "金", "土"], + months: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], + monthsShort: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], + weekShort: '週', + weekStart:0 + }; +}(jQuery)); diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.lt.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.lt.js new file mode 100644 index 00000000..46fc2f1f --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.lt.js @@ -0,0 +1,18 @@ +/** + * Lithuanian translation for bootstrap-year-calendar + * Mantas Urbonas mantas.urbonas@gmail.com + * + * either MIT or BSD or Apache 2 licensed - choose whatever license suits you most. + */ + +;(function($){ + $.fn.calendar.dates['lt'] = { + days: ["Sekmadienis", "Pirmadienis", "Antradienis", "TreÄiadienis", "Ketvirtadienis", "Penktadienis", "Å eÅ¡tadienis"], + daysShort: ["Sek", "Pir", "Ant", "Tre", "Ket", "Pen", "Å eÅ¡"], + daysMin: ["Se", "Pr", "An", "Tr", "Kt", "Pn", "Å e"], + months: ["Sausis", "Vasaris", "Kovas", "Balandis", "Gegužė", "Birželis", "Liepa", "RugpjÅ«tis", "RugsÄ—jis", "Spalis", "Lapkritis", "Gruodis"], + monthsShort: ["Sau", "Vas", "Kov", "Bal", "Geg", "Bir", "Lie", "Rgp", "Rug", "Spa", "Lap", "Gru"], + weekShort: 'Sav', + weekStart: 1 + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.nl.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.nl.js new file mode 100644 index 00000000..e9a90bab --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.nl.js @@ -0,0 +1,19 @@ +/** + * Dutch translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * Dutch translation for bootstrap-datepicker + * Reinier Goltstein + */ + +;(function($){ + $.fn.calendar.dates['nl'] = { + days: ["zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"], + daysShort: ["zo", "ma", "di", "wo", "do", "vr", "za"], + daysMin: ["zo", "ma", "di", "wo", "do", "vr", "za"], + months: ["januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"], + monthsShort: ["jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"], + weekShort: 'w', + weekStart: 1 + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.pl.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.pl.js new file mode 100644 index 00000000..088f5b6d --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.pl.js @@ -0,0 +1,16 @@ +/** + * Polish translation for bootstrap-year-calendar + * Robert 'Wooya' Gaudyn + */ + +;(function($){ + $.fn.calendar.dates['pl'] = { + days: ["Niedziela", "PoniedziaÅ‚ek", "Wtorek", "Åšroda", "Czwartek", "Piatek", "Sobota"], + daysShort: ["Nie", "Pon", "Wto", "Åšro", "Czw", "PiÄ…", "Sob"], + daysMin: ["Ni", "Po", "Wt", "Åšr", "Cz", "Pi", "So"], + months: ["StyczeÅ„", "Luty", "Marzec", "KwiecieÅ„", "Maj", "Czerwiec", "Lipiec", "SierpieÅ„", "WrzesieÅ„", "Październik", "Listopad", "GrudzieÅ„"], + monthsShort: ["Sty", "Lut", "Mar", "Kwi", "Maj", "Cze", "Lip", "Sie", "Wrz", "Paź", "Lis", "Gru"], + weekShort: 'W', + weekStart: 1 + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.pt.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.pt.js new file mode 100644 index 00000000..108903df --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.pt.js @@ -0,0 +1,20 @@ +/** + * Portuguese translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * Portuguese translation for bootstrap-datepicker + * Original code: Cauan Cabral + * Tiago Melo + */ + +;(function($){ + $.fn.calendar.dates['pt'] = { + days: ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"], + daysShort: ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"], + daysMin: ["Do", "Se", "Te", "Qu", "Qu", "Se", "Sa"], + months: ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"], + monthsShort: ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"], + weekShort: 'S', + weekStart:0 + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ru.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ru.js new file mode 100644 index 00000000..01eb290c --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ru.js @@ -0,0 +1,19 @@ +/** + * Russian translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * Russian translation for bootstrap-datepicker + * Victor Taranenko + */ + +;(function($){ + $.fn.calendar.dates['ru'] = { + days: ["ВоÑкреÑенье", "Понедельник", "Вторник", "Среда", "Четверг", "ПÑтница", "Суббота"], + daysShort: ["Ð’Ñк", "Пнд", "Втр", "Срд", "Чтв", "Птн", "Суб"], + daysMin: ["Ð’Ñ", "Пн", "Ð’Ñ‚", "Ср", "Чт", "Пт", "Сб"], + months: ["Январь", "Февраль", "Март", "Ðпрель", "Май", "Июнь", "Июль", "ÐвгуÑÑ‚", "СентÑбрь", "ОктÑбрь", "ÐоÑбрь", "Декабрь"], + monthsShort: ["Янв", "Фев", "Мар", "Ðпр", "Май", "Июн", "Июл", "Ðвг", "Сен", "Окт", "ÐоÑ", "Дек"], + weekShort: 'н', + weekStart: 1 + }; +}(jQuery)); diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.tr.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.tr.js new file mode 100644 index 00000000..bca7e9d4 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.tr.js @@ -0,0 +1,20 @@ +/** + * Turkish translation for bootstrap-year-calendar + * Paul DAVID-SIVELLE + * Based on + * Turkish translation for bootstrap-datepicker + * Serkan Algur + */ + +;(function($){ + $.fn.calendar.dates['tr'] = { + days: ["Pazar", "Pazartesi", "Salı", "ÇarÅŸamba", "PerÅŸembe", "Cuma", "Cumartesi"], + daysShort: ["Pz", "Pzt", "Sal", "ÇrÅŸ", "PrÅŸ", "Cu", "Cts"], + daysMin: ["Pz", "Pzt", "Sa", "Çr", "Pr", "Cu", "Ct"], + months: ["Ocak", "Åžubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "AÄŸustos", "Eylül", "Ekim", "Kasım", "Aralık"], + monthsShort: ["Oca", "Åžub", "Mar", "Nis", "May", "Haz", "Tem", "AÄŸu", "Eyl", "Eki", "Kas", "Ara"], + weekShort: 'H', + weekStart: 1 + }; +}(jQuery)); + diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ua.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ua.js new file mode 100644 index 00000000..60086cb2 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.ua.js @@ -0,0 +1,16 @@ +/** + * Ukrainian translation for bootstrap-year-calendar + * Petro Franko + */ + +;(function($){ + $.fn.calendar.dates['ua'] = { + days: ["ÐеділÑ", "Понеділок", "Вівторок", "Середа", "Четвер", "П'ÑтницÑ", "Субота"], + daysShort: ["Ðед", "Пон", "Ð’Ñ‚", "Сер", "Чет", "Пт", "Суб"], + daysMin: ["Ðд", "Пн", "Ð’Ñ‚", "Ср", "Чт", "Пт", "Сб"], + months: ["Січень", "Лютий", "Березень", "Квітень", "Травень", "Червень", "Липень", "Серпень", "ВереÑень", "Жовтень", "ЛиÑтопад", "Грудень"], + monthsShort: ["Січ", "Лют", "Бер", "Кві", "Тра", "Чер", "Лип", "Сер", "Вер", "Жов", "ЛиÑ", "Гру"], + weekShort: 'Т', + weekStart: 1 + }; +}(jQuery)); diff --git a/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.uk.js b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.uk.js new file mode 100644 index 00000000..b3957fd8 --- /dev/null +++ b/swh/web/static/js/bootstrap-year-calendar/languages/bootstrap-year-calendar.uk.js @@ -0,0 +1,16 @@ +/** + * Ukrainian translation for bootstrap-year-calendar + * by Vadym Korolyuk + */ + +;(function($){ + $.fn.calendar.dates['uk'] = { + days: ["ÐеділÑ", "Понеділок", "Вівторок", "Середа", "Четвер", "П'ÑтницÑ", "Субота"], + daysShort: ["Ðед", "Пон", "Вів", "Сер", "Чет", "Птн", "Суб"], + daysMin: ["Ðд", "Пн", "Ð’Ñ‚", "Ср", "Чт", "Пт", "Сб"], + months: ["Січень", "Лютий", "Березень", "Квітень", "Травень", "Червень", "Липень","Серпень", "ВереÑень", "Жовтень", "ЛиÑтопад", "Грудень"], + monthsShort: ["Січ", "Лют", "Бер", "Кві", "Тра", "Чер", "Лип", "Сер", "Вер", "Жов", "ЛиÑ","Гру"], + weekShort: 'н', + weekStart: 1 + }; +}(jQuery)); \ No newline at end of file diff --git a/swh/web/static/js/origin_visits_histogram.js b/swh/web/static/js/origin_visits_histogram.js new file mode 100644 index 00000000..f259ba28 --- /dev/null +++ b/swh/web/static/js/origin_visits_histogram.js @@ -0,0 +1,345 @@ +// Creation of a stacked histogram with D3.js for SWH origin visits history +// Parameters description: +// - container: selector for the div that will contain the histogram +// - visits_data: raw swh origin visits data +// - current_year: the visits year to display by default +// - year_click_callback: callback when the user selects a year through the histogram + +function create_visits_histogram(container, visits_data, current_year, year_click_callback) { + + // remove previously created hisogram and tooltip if any + d3.select(container).select('svg').remove(); + d3.select('div.d3-tooltip').remove(); + + // histogram size and margins + var width = 1000, height = 300, + margin = {top: 20, right: 80, bottom: 30, left: 50}; + + // create responsive svg + var svg = d3.select(container) + .attr("style", + "padding-bottom: " + Math.ceil(height * 100 / width) + "%") + .append("svg") + .attr("viewBox", "0 0 " + width + " " + height); + + // create tooltip div + var tooltip = d3.select("body") + .append("div") + .attr("class", "d3-tooltip") + .style("opacity", 0); + + // update width and height without margins + width = width - margin.left - margin.right, + height = height - margin.top - margin.bottom; + + // create main svg group element + var g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + // create x scale + var x = d3.scaleTime().rangeRound([0, width]); + + // create y scale + var y = d3.scaleLinear().range([height, 0]); + + // create oridinal colorscale mapping visit status + var colors = d3.scaleOrdinal() + .domain(["full", "partial", "failed", "ongoing"]) + .range(["#008000", "#edc344", "#ff0000", "#0000ff"]); + + // first SWH crawls were made in 2015 + var startYear = 2015; + // set latest display year as the current one + var now = new Date(); + var endYear = now.getUTCFullYear()+1; + var monthExtent = [new Date(Date.UTC(startYear, 0, 1)), new Date(Date.UTC(endYear, 0, 1))]; + + var currentYear = current_year; + + // create months bins based on setup extent + var monthBins = d3.timeMonths(d3.timeMonth.offset(monthExtent[0], -1), monthExtent[1]); + // create years bins based on setup extent + var yearBins = d3.timeYears(monthExtent[0], monthExtent[1]); + + // set x scale domain + x.domain(d3.extent(monthBins)); + + // use D3 histogram layout to create a function that will bin the visits by month + var binByMonth = d3.histogram() + .value(function(d) { + return d.date; + }) + .domain(x.domain()) + .thresholds(monthBins); + + // use D3 nest function to group the visits by status + var visitsByStatus = d3.nest() + .key(function(d) { + return d["status"] + }) + .sortKeys(d3.ascending) + .entries(visits_data); + + // prepare data in order to be able to stack visit statuses by month + var statuses = []; + var histData = []; + for (var i = 0 ; i < monthBins.length ; ++i) { + histData[i] = {}; + } + visitsByStatus.forEach(function(entry) { + statuses.push(entry.key); + var monthsData = binByMonth(entry.values); + for (var i = 0 ; i < monthsData.length ; ++i) { + histData[i]['x0'] = monthsData[i]['x0']; + histData[i]['x1'] = monthsData[i]['x1']; + histData[i][entry.key] = monthsData[i]; + } + }); + + // create function to stack visits statuses by month + var stacked = d3.stack() + .keys(statuses) + .value(function(d, key) { + return d[key].length; + }); + + // compute the maximum amount of visits by month + var yMax = d3.max(histData, function(d) { + var total = 0; + for (var i = 0 ; i < statuses.length ; ++i) { + total += d[statuses[i]].length; + } + return total; + }); + + // set y scale domain + y.domain([0, yMax]); + + // compute ticks values for the y axis + var step = 5; + var yTickValues = []; + for (var i = 0 ; i <= yMax/step ; ++i) { + yTickValues.push(i*step); + } + if (yTickValues.length == 0) { + for (var i = 0 ; i <= yMax ; ++i) { + yTickValues.push(i); + } + } else if (yMax%step != 0) { + yTickValues.push(yMax); + } + + // add histogram background grid + g.append("g") + .attr("class", "grid") + .call(d3.axisLeft(y) + .tickValues(yTickValues) + .tickSize(-width) + .tickFormat("")) + + // create one fill only rectangle by displayed year + // each rectangle will be made visible when hovering the mouse over a year range + // user will then be able to select a year by clicking in the rectangle + g.append("g") + .selectAll("rect") + .data(yearBins) + .enter().append("rect") + .attr("class", function(d) {return "year" + d.getUTCFullYear()}) + .attr("fill", 'red') + .attr('fill-opacity', function(d) { + return d.getUTCFullYear() == currentYear ? 0.3 : 0; + }) + .attr('stroke', 'none') + .attr("x", function(d) { + var date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + return x(date); + }) + .attr("y", 0) + .attr("height", height) + .attr("width", function(d) { + var date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + var yearWidth = x(d3.timeYear.offset(date, 1)) - x(date); + return yearWidth; + }) + // mouse event callbacks used to show rectangle years + // when hovering the mouse over the histograms + .on("mouseover", function(d) { + svg.selectAll('rect.year' + d.getUTCFullYear()) + .attr('fill-opacity', 0.5); + }) + .on("mouseout", function(d) { + svg.selectAll('rect.year' + d.getUTCFullYear()) + .attr('fill-opacity', 0); + svg.selectAll('rect.year' + currentYear) + .attr('fill-opacity', 0.3); + }) + // callback to select a year after a mouse click + // in a rectangle year + .on("click", function(d) { + svg.selectAll('rect.year' + currentYear) + .attr('fill-opacity', 0); + svg.selectAll('rect.yearoutline' + currentYear) + .attr('stroke', 'none'); + currentYear = d.getUTCFullYear(); + svg.selectAll('rect.year' + currentYear) + .attr('fill-opacity', 0.5); + svg.selectAll('rect.yearoutline' + currentYear) + .attr('stroke', 'black'); + year_click_callback(currentYear); + }); + + // create the stacked histogram of visits + g.append("g") + .selectAll("g") + .data(stacked(histData)) + .enter().append("g") + .attr("fill", function(d) { return colors(d.key); }) + .selectAll("rect") + .data(function(d) { return d; }) + .enter().append("rect") + .attr("class", function(d) { return "month" + d.data.x1.getMonth(); }) + .attr("x", function(d) { + return x(d.data.x0); + }) + .attr("y", function(d) { return y(d[1]); }) + .attr("height", function(d) { return y(d[0]) - y(d[1]); }) + .attr("width", function(d) { + return x(d.data.x1) - x(d.data.x0) - 1; + }) + // mouse event callbacks used to show rectangle years + // but also to show tooltips when hovering the mouse + // over the histogram bars + .on("mouseover", function(d) { + svg.selectAll('rect.year' + d.data.x1.getUTCFullYear()) + .attr('fill-opacity', 0.5); + tooltip.transition() + .duration(200) + .style("opacity", 1); + var ds = d.data.x1.toISOString().substr(0, 7).split('-'); + var tootltip_text = '' + ds[1] + ' / ' + ds[0] + ':
'; + for (var i = 0 ; i < statuses.length ; ++i) { + var visit_status = statuses[i]; + var nb_visits = d.data[visit_status].length; + if (nb_visits == 0) continue; + tootltip_text += nb_visits + ' ' + visit_status + ' visits'; + if (i != statuses.length - 1) tootltip_text += '
'; + } + tooltip.html(tootltip_text) + .style("left", d3.event.pageX + 15 + "px") + .style("top", d3.event.pageY + "px"); + }) + .on("mouseout", function(d) { + svg.selectAll('rect.year' + d.data.x1.getUTCFullYear()) + .attr('fill-opacity', 0); + svg.selectAll('rect.year' + currentYear) + .attr('fill-opacity', 0.3); + tooltip.transition() + .duration(500) + .style("opacity", 0); + }) + .on("mousemove", function() { + tooltip.style("left", d3.event.pageX + 15 + "px") + .style("top", d3.event.pageY + "px"); + }) + // callback to select a year after a mouse click + // inside a histogram bar + .on("click", function(d) { + svg.selectAll('rect.year' + currentYear) + .attr('fill-opacity', 0); + svg.selectAll('rect.yearoutline' + currentYear) + .attr('stroke', 'none'); + currentYear = d.data.x1.getUTCFullYear(); + svg.selectAll('rect.year' + currentYear) + .attr('fill-opacity', 0.5); + svg.selectAll('rect.yearoutline' + currentYear) + .attr('stroke', 'black'); + year_click_callback(currentYear); + }); + + // create one stroke only rectangle by displayed year + // that will be displayed on top of the histogram when the user has selected a year + g.append("g") + .selectAll("rect") + .data(yearBins) + .enter().append("rect") + .attr("class", function(d) {return "yearoutline" + d.getUTCFullYear()}) + .attr("fill", 'none') + .attr('stroke', function(d) { + return d.getUTCFullYear() == currentYear ? 'black' : 'none'; + }) + .attr("x", function(d) { + var date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + return x(date); + }) + .attr("y", 0) + .attr("height", height) + .attr("width", function(d) { + var date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + var yearWidth = x(d3.timeYear.offset(date, 1)) - x(date); + return yearWidth; + }); + + // add x axis with a tick for every 1st day of each year + var xAxis = g.append("g") + .attr("class", "axis") + .attr("transform", "translate(0," + height + ")") + .call(d3.axisBottom(x) + .ticks(d3.timeYear.every(1)) + .tickFormat(function(d) { + return d.getUTCFullYear(); + })); + + // shift tick labels in order to display them at the middle + // of each year range + xAxis.selectAll("text") + .attr("transform", function(d) { + var year = d.getUTCFullYear(); + var date = new Date(Date.UTC(year, 0, 1)); + var yearWidth = x(d3.timeYear.offset(date, 1)) - x(date); + return "translate(" + -yearWidth/2 + ", 0)" + }); + + // add y axis for the number of visits + g.append("g") + .attr("class", "axis") + .call(d3.axisLeft(y).tickValues(yTickValues)); + + // add legend for visit statuses + var legendGroup = g.append("g") + .attr("font-family", "sans-serif") + .attr("font-size", 10) + .attr("text-anchor", "end"); + + legendGroup.append('text') + .attr("x", width+margin.right-5) + .attr("y", 9.5) + .attr("dy", "0.32em") + .text('visit status:'); + + var legend = legendGroup.selectAll("g") + .data(statuses.slice().reverse()) + .enter().append("g") + .attr("transform", function(d, i) { + return "translate(0," + (i+1) * 20 + ")"; + }); + + legend.append("rect") + .attr("x", width+2*margin.right/3) + .attr("width", 19) + .attr("height", 19) + .attr("fill", colors); + + legend.append("text") + .attr("x", width+2*margin.right/3 - 5) + .attr("y", 9.5) + .attr("dy", "0.32em") + .text(function(d) { return d; }); + + // add text label for the y axis + g.append("text") + .attr("transform", "rotate(-90)") + .attr("y", -margin.left) + .attr("x", -(height / 2)) + .attr("dy", "1em") + .style("text-anchor", "middle") + .text("Number of visits"); +} \ No newline at end of file diff --git a/swh/web/templates/layout.html b/swh/web/templates/layout.html index 86c6ee5d..b30f6468 100644 --- a/swh/web/templates/layout.html +++ b/swh/web/templates/layout.html @@ -1,77 +1,77 @@ {% load static %} {% block title %}{% endblock %} {% block header %}{% endblock %} - - + +
{% block content %}{% endblock %}
back to top
diff --git a/swh/web/templates/origin.html b/swh/web/templates/origin.html index c3e7883e..be080f65 100644 --- a/swh/web/templates/origin.html +++ b/swh/web/templates/origin.html @@ -1,116 +1,345 @@ {% extends "browse.html" %} {% load static %} {% load swh_templatetags %} {% block swh-browse-before-panels %}
{% if origin_info.url|slice:"0:4" == "http" %} {% endif %}

SWH origin: {{ origin_info.url }}

{% for key, val in swh_object_metadata.items|dictsort:"0.lower" %} {% endfor %}
{% endblock %} {% block swh-browse-main-panel-content %}

{{ top_panel_text }}

- {% if origin_visits_data %} - - -

Calendar

-
-
-
-
-
- -
- -

List

- {% for visits_by_year in visits_splitted %} -
-
- {% for key, val in visits_by_year.items|dictsortreversed:"0.lower" %} -
- - - - - - - - {% for visit in val %} - - - - {% endfor %} - -
{{ key }}
- {{ visit.fmt_date }} -
-
- {% endfor %} -
-
- {% endfor %} - - - - - - - - - +

Visits overview

+ +
    +
  • Total number of visits: {{ origin_visits|length }}
  • +
  • Last full visit:
  • +
  • First full visit:
  • +
  • Last visit:
  • +
+ +

Visits history

+ +
+ + + +
+ +

Timeline

+ +
- {% else %} +

Calendar

-

- Origin has not yet been visited by Software Heritage. -

+
- {% endif %} +

List

+ +
+ + + + + + + {% endblock %} diff --git a/swh/web/tests/browse/views/test_origin.py b/swh/web/tests/browse/views/test_origin.py index 7f5e0943..b3ad21fe 100644 --- a/swh/web/tests/browse/views/test_origin.py +++ b/swh/web/tests/browse/views/test_origin.py @@ -1,722 +1,710 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information # flake8: noqa from unittest.mock import patch from nose.tools import istest, nottest from django.test import TestCase from django.utils.html import escape from swh.web.common.exc import NotFoundExc from swh.web.common.utils import ( reverse, gen_path_info, format_utc_iso_date, parse_timestamp ) from swh.web.tests.testbase import SWHWebTestBase from .data.origin_test_data import ( origin_info_test_data, origin_visits_test_data, stub_content_origin_info, stub_content_origin_visit_id, stub_content_origin_visit_unix_ts, stub_content_origin_visit_iso_date, stub_content_origin_branch, stub_content_origin_visits, stub_content_origin_snapshot, stub_origin_info, stub_visit_id, stub_origin_visits, stub_origin_snapshot, stub_origin_root_directory_entries, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_path, stub_origin_sub_directory_entries, stub_visit_unix_ts, stub_visit_iso_date ) from .data.content_test_data import ( stub_content_root_dir, stub_content_text_data, stub_content_text_path ) class SwhBrowseOriginTest(SWHWebTestBase, TestCase): @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.service') @istest def origin_browse(self, mock_service, mock_get_origin_visits): mock_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_visits.return_value = origin_visits_test_data url = reverse('browse-origin', kwargs={'origin_type': origin_info_test_data['type'], 'origin_url': origin_info_test_data['url']}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('origin.html') self.assertContains(resp, '
%s
' % origin_info_test_data['type']) self.assertContains(resp, '
%s
' % (origin_info_test_data['url'], origin_info_test_data['url'])) - self.assertContains(resp, '', - count=len(origin_visits_test_data)) - - for visit in origin_visits_test_data: - visit_date_iso = format_utc_iso_date(visit['date'], '%Y-%m-%dT%H:%M:%SZ') - visit_date = format_utc_iso_date(visit['date']) - browse_url = reverse('browse-origin-directory', - kwargs={'origin_type': origin_info_test_data['type'], - 'origin_url': origin_info_test_data['url'], - 'timestamp': visit_date_iso}) - self.assertContains(resp, 'href="%s">%s' % - (browse_url, visit_date)) @nottest def origin_content_view_test(self, origin_info, origin_visits, origin_branches, origin_releases, origin_branch, root_dir_sha1, content_sha1, content_path, content_data, content_language, visit_id=None, timestamp=None): url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url'], 'path': content_path} if not visit_id: visit_id = origin_visits[-1]['visit'] query_params = {} if timestamp: url_args['timestamp'] = timestamp if visit_id: query_params['visit_id'] = visit_id url = reverse('browse-origin-content', kwargs=url_args, query_params=query_params) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '' % content_language) self.assertContains(resp, escape(content_data)) split_path = content_path.split('/') filename = split_path[-1] path = content_path.replace(filename, '')[:-1] path_info = gen_path_info(path) del url_args['path'] if timestamp: url_args['timestamp'] = \ format_utc_iso_date(parse_timestamp(timestamp).isoformat(), '%Y-%m-%dT%H:%M:%S') root_dir_url = reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) self.assertContains(resp, '
  • ', count=len(path_info)+1) self.assertContains(resp, '%s' % (root_dir_url, root_dir_sha1[:7])) for p in path_info: url_args['path'] = p['path'] dir_url = reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) self.assertContains(resp, '%s' % (dir_url, p['name'])) self.assertContains(resp, '
  • %s
  • ' % filename) query_string = 'sha1_git:' + content_sha1 url_raw = reverse('browse-content-raw', kwargs={'query_string': query_string}, query_params={'filename': filename}) self.assertContains(resp, url_raw) del url_args['path'] origin_branches_url = \ reverse('browse-origin-branches', kwargs=url_args, query_params=query_params) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', kwargs=url_args, query_params=query_params) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) self.assertContains(resp, '
  • ', count=len(origin_branches)) url_args['path'] = content_path for branch in origin_branches: query_params['branch'] = branch['name'] root_dir_branch_url = \ reverse('browse-origin-content', kwargs=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_branch_url) self.assertContains(resp, '
  • ', count=len(origin_releases)) query_params['branch'] = None for release in origin_releases: query_params['release'] = release['name'] root_dir_release_url = \ reverse('browse-origin-content', kwargs=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_release_url) @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.views.utils.snapshot_context.service') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.utils.snapshot_context.request_content') @istest def origin_content_view(self, mock_request_content, mock_utils_service, mock_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): stub_content_text_sha1 = stub_content_text_data['checksums']['sha1'] mock_get_origin_visits.return_value = stub_content_origin_visits mock_get_origin_visit_snapshot.return_value = stub_content_origin_snapshot mock_service.lookup_directory_with_path.return_value = \ {'target': stub_content_text_sha1} mock_request_content.return_value = stub_content_text_data mock_utils_service.lookup_origin.return_value = stub_content_origin_info self.origin_content_view_test(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp') self.origin_content_view_test(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', visit_id=stub_content_origin_visit_id) self.origin_content_view_test(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', timestamp=stub_content_origin_visit_unix_ts) self.origin_content_view_test(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', timestamp=stub_content_origin_visit_iso_date) @nottest def origin_directory_view(self, origin_info, origin_visits, origin_branches, origin_releases, origin_branch, root_directory_sha1, directory_entries, visit_id=None, timestamp=None, path=None): dirs = [e for e in directory_entries if e['type'] == 'dir'] files = [e for e in directory_entries if e['type'] == 'file'] if not visit_id: visit_id = origin_visits[-1]['visit'] url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url']} query_params = {} if timestamp: url_args['timestamp'] = timestamp else: query_params['visit_id'] = visit_id if path: url_args['path'] = path url = reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('directory.html') self.assertContains(resp, '', count=len(dirs)) self.assertContains(resp, '', count=len(files)) if timestamp: url_args['timestamp'] = \ format_utc_iso_date(parse_timestamp(timestamp).isoformat(), '%Y-%m-%dT%H:%M:%S') for d in dirs: dir_path = d['name'] if path: dir_path = "%s/%s" % (path, d['name']) dir_url_args = dict(url_args) dir_url_args['path'] = dir_path dir_url = reverse('browse-origin-directory', kwargs=dir_url_args, query_params=query_params) self.assertContains(resp, dir_url) for f in files: file_path = f['name'] if path: file_path = "%s/%s" % (path, f['name']) file_url_args = dict(url_args) file_url_args['path'] = file_path file_url = reverse('browse-origin-content', kwargs=file_url_args, query_params=query_params) self.assertContains(resp, file_url) if 'path' in url_args: del url_args['path'] root_dir_branch_url = \ reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) nb_bc_paths = 1 if path: nb_bc_paths = len(path.split('/')) + 1 self.assertContains(resp, '
  • ', count=nb_bc_paths) self.assertContains(resp, '%s' % (root_dir_branch_url, root_directory_sha1[:7])) origin_branches_url = \ reverse('browse-origin-branches', kwargs=url_args, query_params=query_params) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', kwargs=url_args, query_params=query_params) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) if path: url_args['path'] = path self.assertContains(resp, '
  • ', count=len(origin_branches)) for branch in origin_branches: query_params['branch'] = branch['name'] root_dir_branch_url = \ reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_branch_url) self.assertContains(resp, '
  • ', count=len(origin_releases)) query_params['branch'] = None for release in origin_releases: query_params['release'] = release['name'] root_dir_release_url = \ reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_release_url) self.assertContains(resp, 'vault-cook-directory') self.assertContains(resp, 'vault-cook-revision') @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') @istest def origin_root_directory_view(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_directory.return_value = \ stub_origin_root_directory_entries mock_utils_service.lookup_origin.return_value = stub_origin_info self.origin_directory_view(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries) self.origin_directory_view(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, visit_id=stub_visit_id) self.origin_directory_view(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_unix_ts) self.origin_directory_view(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_iso_date) @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.utils.snapshot_context.service') @istest def origin_sub_directory_view(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_directory.return_value = \ stub_origin_sub_directory_entries mock_origin_service.lookup_directory_with_path.return_value = \ {'target': '120c39eeb566c66a77ce0e904d29dfde42228adb'} mock_utils_service.lookup_origin.return_value = stub_origin_info self.origin_directory_view(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, path=stub_origin_sub_directory_path) self.origin_directory_view(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, visit_id=stub_visit_id, path=stub_origin_sub_directory_path) self.origin_directory_view(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_unix_ts, path=stub_origin_sub_directory_path) self.origin_directory_view(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_iso_date, path=stub_origin_sub_directory_path) @patch('swh.web.browse.views.utils.snapshot_context.request_content') @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') @patch('swh.web.browse.views.utils.snapshot_context.service') @istest def origin_request_errors(self, mock_snapshot_service, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits, mock_request_content): mock_origin_service.lookup_origin.side_effect = \ NotFoundExc('origin not found') url = reverse('browse-origin', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEquals(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'origin not found', status_code=404) mock_utils_service.lookup_origin.side_effect = None mock_utils_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_visits.return_value = [] url = reverse('browse-origin-directory', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEquals(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, "No SWH visit", status_code=404) mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = \ NotFoundExc('visit not found') url = reverse('browse-origin-directory', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}, query_params={'visit_id': len(stub_origin_visits)+1}) resp = self.client.get(url) self.assertEquals(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Visit.*not found') mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = None mock_get_origin_visit_snapshot.return_value = ([], []) url = reverse('browse-origin-directory', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEquals(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Origin.*has an empty list of branches') mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_directory.side_effect = \ NotFoundExc('Directory not found') url = reverse('browse-origin-directory', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEquals(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Directory not found', status_code=404) mock_origin_service.lookup_origin.side_effect = None mock_origin_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_visits.return_value = [] url = reverse('browse-origin-content', kwargs={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'foo'}) resp = self.client.get(url) self.assertEquals(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, "No SWH visit", status_code=404) mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = \ NotFoundExc('visit not found') url = reverse('browse-origin-content', kwargs={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'foo'}, query_params={'visit_id': len(stub_origin_visits)+1}) resp = self.client.get(url) self.assertEquals(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Visit.*not found') mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = None mock_get_origin_visit_snapshot.return_value = ([], []) url = reverse('browse-origin-content', kwargs={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'baz'}) resp = self.client.get(url) self.assertEquals(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Origin.*has an empty list of branches') mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_snapshot_service.lookup_directory_with_path.return_value = \ {'target': stub_content_text_data['checksums']['sha1']} mock_request_content.side_effect = \ NotFoundExc('Content not found') url = reverse('browse-origin-content', kwargs={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'baz'}) resp = self.client.get(url) self.assertEquals(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Content not found', status_code=404) @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') @istest def origin_branches(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_origin.return_value = stub_origin_info url_args = {'origin_type': stub_origin_info['type'], 'origin_url': stub_origin_info['url']} url = reverse('browse-origin-branches', kwargs=url_args) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('branches.html') origin_branches = stub_origin_snapshot[0] origin_releases = stub_origin_snapshot[1] origin_branches_url = \ reverse('browse-origin-branches', kwargs=url_args) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', kwargs=url_args) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) self.assertContains(resp, '', count=len(origin_branches)) for branch in origin_branches: browse_branch_url = reverse('browse-origin-directory', kwargs={'origin_type': stub_origin_info['type'], 'origin_url': stub_origin_info['url']}, query_params={'branch': branch['name']}) self.assertContains(resp, '%s' % (escape(browse_branch_url), branch['name'])) browse_revision_url = reverse('browse-revision', kwargs={'sha1_git': branch['revision']}, query_params={'origin_type': stub_origin_info['type'], 'origin_url': stub_origin_info['url']}) self.assertContains(resp, '%s' % (escape(browse_revision_url), branch['revision'][:7])) @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') @istest def origin_releases(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_origin.return_value = stub_origin_info url_args = {'origin_type': stub_origin_info['type'], 'origin_url': stub_origin_info['url']} url = reverse('browse-origin-releases', kwargs=url_args) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('releases.html') origin_branches = stub_origin_snapshot[0] origin_releases = stub_origin_snapshot[1] origin_branches_url = \ reverse('browse-origin-branches', kwargs=url_args) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', kwargs=url_args) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) self.assertContains(resp, '', count=len(origin_releases)) for release in origin_releases: browse_release_url = reverse('browse-release', kwargs={'sha1_git': release['id']}, query_params={'origin_type': stub_origin_info['type'], 'origin_url': stub_origin_info['url']}) self.assertContains(resp, '%s' % (escape(browse_release_url), release['name']))