mirror of https://codeberg.org/dribdat/dribdat.git
commit
911d936b02
|
@ -14,7 +14,7 @@ The philosophy of this project, in a nutshell, is: **live and let live** (no tec
|
|||
|
||||
> Look at [Screenshots and examples](https://dribdat.cc/about#screenshots) in the User Guide.
|
||||
|
||||
For more background and references, see the [User Handbook](https://docs.dribdat.cc/usage/). If you need help or advice in setting up your event, or would like to contribute to the project: please get in touch via [the website](https://dribd.at) or [GitHub Issues](https://github.com/dribdat/dribdat/issues). Follow and support the project's development on [OpenCollective](https://opencollective.com/dribdat/updates)
|
||||
For more background and references, see the [User Handbook](https://docs.dribdat.cc/usage/). If you need help or advice in setting up your event, or would like to contribute to the project: please get in touch via [Discussions](https://github.com/orgs/dribdat/discussions) or [GitHub Issues](https://github.com/dribdat/dribdat/issues). Follow and support the project's development on [OpenCollective](https://opencollective.com/dribdat/updates)
|
||||
|
||||
<a href="https://opencollective.com/dribdat/donate" target="_blank"><img src="https://opencollective.com/dribdat/donate/button@2x.png?color=blue" width=300 /></a>
|
||||
|
||||
|
@ -28,7 +28,7 @@ If you would like to run dribdat on any other cloud or local machine, there are
|
|||
|
||||
See also **[backboard](https://github.com/dribdat/backboard)**, a sample responsive web application, and our **[dridbot](https://github.com/dribdat/dridbot)** chat client, which both demonstrate reuse of the dribdat API.
|
||||
|
||||
If you need support with your deployment, please reach out through the Issues board, or use the contact form on our website: [dribd.at](https://dribd.at)
|
||||
If you need support with your deployment, please reach out through [Discussions](https://github.com/orgs/dribdat/discussions).
|
||||
|
||||
# Credits
|
||||
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
import json, csv
|
||||
import tempfile
|
||||
from os import path
|
||||
from copy import deepcopy
|
||||
from werkzeug.utils import secure_filename
|
||||
from datetime import datetime as dtt
|
||||
from frictionless import Package, Resource
|
||||
|
@ -169,18 +170,29 @@ def import_user_roles(user, new_roles, dry_run=False):
|
|||
return updates
|
||||
|
||||
|
||||
def import_project_data(data, dry_run=False):
|
||||
def import_project_data(data, dry_run=False, event=None):
|
||||
"""Collect data of a list of projects."""
|
||||
updates = []
|
||||
for pjt in data:
|
||||
# Skip empty rows
|
||||
if not 'name' in pjt:
|
||||
logging.warning('Skipping empty row')
|
||||
logging.debug(pjt)
|
||||
continue
|
||||
# Get project name and content
|
||||
name = pjt['name']
|
||||
if not 'longtext' in pjt and 'excerpt' in pjt:
|
||||
pjt['longtext'] = pjt.pop('excerpt')
|
||||
# Search for event
|
||||
event_name = pjt['event_name']
|
||||
event = Event.query.filter_by(name=event_name).first()
|
||||
event_name = None
|
||||
if 'event_name' in pjt:
|
||||
event_name = pjt['event_name']
|
||||
if event_name and (not event or event.name != event_name):
|
||||
event = Event.query.filter_by(name=event_name).first()
|
||||
if not event:
|
||||
logging.warning('Error: event not found: %s' % event_name)
|
||||
logging.warning('Skip [%s], event not found: %s' % (name, event_name))
|
||||
continue
|
||||
# Search for project
|
||||
name = pjt['name']
|
||||
project = Project.query.filter_by(name=name).first()
|
||||
if not project:
|
||||
logging.info('Creating project: %s' % name)
|
||||
|
@ -188,7 +200,7 @@ def import_project_data(data, dry_run=False):
|
|||
else:
|
||||
logging.info('Updating project: %s' % name)
|
||||
project.set_from_data(pjt)
|
||||
project.event = event
|
||||
project.event_id = event.id
|
||||
if not dry_run:
|
||||
project.save()
|
||||
updates.append(project.data)
|
||||
|
@ -287,3 +299,17 @@ def load_file_datapackage(filepath, dry_run=True, all_data=False):
|
|||
return import_event_package(data, dry_run, all_data)
|
||||
except json.decoder.JSONDecodeError:
|
||||
return {'errors': ['Could not load package due to JSON error']}
|
||||
|
||||
|
||||
def import_projects_csv(filedata, event=None, dry_run=True):
|
||||
"""Save a temporary CSV file and import project data to event."""
|
||||
ext = filedata.filename.split('.')[-1].lower()
|
||||
if ext not in ['csv']:
|
||||
return {'errors': ['Invalid format (allowed: CSV)']}
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
filepath = path.join(tmpdir, secure_filename(filedata.filename))
|
||||
filedata.save(filepath)
|
||||
with open(filepath, mode='r') as csvfile:
|
||||
csvreader = csv.DictReader(csvfile)
|
||||
projdata = [ deepcopy(row) for row in csvreader ]
|
||||
return { 'projects': import_project_data(projdata, dry_run, event) }
|
||||
|
|
|
@ -166,3 +166,18 @@ def gen_csv(csvdata):
|
|||
writer.writerow(rk)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def event_upload_configuration(import_level='test'):
|
||||
"""Configure the upload."""
|
||||
dry_run = True
|
||||
all_data = False
|
||||
if import_level == 'basic':
|
||||
dry_run = False
|
||||
status = "Basic"
|
||||
elif import_level == 'full':
|
||||
dry_run = False
|
||||
all_data = True
|
||||
status = "Complete"
|
||||
else:
|
||||
status = "Preview"
|
||||
return dry_run, all_data, status
|
||||
|
|
|
@ -59,7 +59,7 @@ class PkModel(Model):
|
|||
isinstance(record_id, (int, float)),
|
||||
)
|
||||
):
|
||||
return cls.query.get(int(record_id))
|
||||
return db.session.get(cls, int(record_id))
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -27,11 +27,12 @@ from ..apiutils import (
|
|||
get_project_list,
|
||||
get_event_activities,
|
||||
get_schema_for_user_projects,
|
||||
event_upload_configuration,
|
||||
expand_project_urls,
|
||||
gen_csv,
|
||||
)
|
||||
from ..apipackage import (
|
||||
fetch_datapackage, import_datapackage
|
||||
fetch_datapackage, import_datapackage, import_projects_csv
|
||||
)
|
||||
|
||||
blueprint = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
@ -309,7 +310,6 @@ def project_search_json():
|
|||
|
||||
# ------ UPDATE ---------
|
||||
|
||||
|
||||
@blueprint.route('/event/load/datapackage', methods=["POST"])
|
||||
@admin_required
|
||||
def event_upload_datapackage():
|
||||
|
@ -318,20 +318,13 @@ def event_upload_datapackage():
|
|||
import_level = request.form.get('import')
|
||||
if not filedata or not import_level:
|
||||
return jsonify(status='Error', errors=['Missing import data parameters'])
|
||||
# Configuration
|
||||
dry_run, all_data, status = event_upload_configuration(import_level)
|
||||
# Check link
|
||||
if filedata.filename.endswith('.csv'):
|
||||
return event_push_csv(filedata, dry_run)
|
||||
if 'datapackage.json' not in filedata.filename:
|
||||
return jsonify(status='Error', errors=['Must be a datapackage.json'])
|
||||
# Configuration
|
||||
dry_run = True
|
||||
all_data = False
|
||||
status = "Preview"
|
||||
if import_level == 'basic':
|
||||
dry_run = False
|
||||
status = "Basic"
|
||||
if import_level == 'full':
|
||||
dry_run = False
|
||||
all_data = True
|
||||
status = "Complete"
|
||||
# File handling
|
||||
results = import_datapackage(filedata, dry_run, all_data)
|
||||
event_names = ', '.join([r['name'] for r in results['events']])
|
||||
|
@ -345,7 +338,7 @@ def event_upload_datapackage():
|
|||
|
||||
@blueprint.route('/event/push/datapackage', methods=["PUT", "POST"])
|
||||
def event_push_datapackage():
|
||||
"""Upload event data from a Data Package."""
|
||||
"""Upload event data from a Data Package (auth by key)."""
|
||||
key = request.headers.get('key')
|
||||
if not key or key != current_app.config['SECRET_API']:
|
||||
return jsonify(status='Error', errors=['Invalid API key'])
|
||||
|
@ -356,6 +349,28 @@ def event_push_datapackage():
|
|||
return jsonify(status='Complete', results=results)
|
||||
|
||||
|
||||
def event_push_csv(filedata, dry_run=False):
|
||||
"""Upload event data from a CSV file."""
|
||||
if not filedata or not filedata.filename:
|
||||
return jsonify(status='Error', errors=['Missing import data parameters'])
|
||||
if not filedata.filename.endswith('.csv'):
|
||||
return jsonify(status='Error', errors=['You must provide a CSV file'])
|
||||
# Find the event
|
||||
event_id = request.form.get('event')
|
||||
if event_id:
|
||||
event = Event.query.filter_by(id=event_id).first_or_404()
|
||||
else:
|
||||
event = Event.query.order_by(Event.id.desc()).first_or_404()
|
||||
# File handling
|
||||
results = import_projects_csv(filedata, event, dry_run)
|
||||
if 'errors' in results:
|
||||
return jsonify(status='Error', errors=results['errors'])
|
||||
else:
|
||||
flash("Event projects uploaded: %s (%d)" % (event.name, len(results)), 'success')
|
||||
return redirect(url_for("admin.events"))
|
||||
return jsonify(status=status, results=results)
|
||||
|
||||
|
||||
@blueprint.route('/event/current/get/status', methods=["GET"])
|
||||
def event_get_status():
|
||||
"""Get current event status."""
|
||||
|
|
|
@ -158,7 +158,7 @@ def project_action(project_id, of_type=None, as_view=True, then_redirect=False,
|
|||
else:
|
||||
missing_roles = None
|
||||
|
||||
# Select available project image
|
||||
# Select an image for the META tags
|
||||
if project.image_url:
|
||||
project_image_url = project.image_url
|
||||
elif event.logo_url:
|
||||
|
|
|
@ -2081,8 +2081,7 @@ SlackIn button
|
|||
|
||||
.account-register a { margin-left: 3.5em; font-size: 125%; }
|
||||
|
||||
.account-register::before,
|
||||
.sso-login::after {
|
||||
.account-register::before {
|
||||
content: 'or';
|
||||
display: block;
|
||||
margin: 0.5em 5em;
|
||||
|
@ -2090,9 +2089,5 @@ SlackIn button
|
|||
font-family: cursive;
|
||||
font-style: italic;
|
||||
}
|
||||
.sso-login::after {
|
||||
margin-top: 1em;
|
||||
margin-bottom: -0.5em;
|
||||
}
|
||||
|
||||
.__slackin { margin-bottom: 10px; display: block; }
|
||||
|
|
|
@ -304,7 +304,8 @@
|
|||
e.preventDefault();
|
||||
var url = $form.attr('action');
|
||||
$form.find('input[type="submit"]').addClass('disabled');
|
||||
$form.find('.message-ok,.message-error').hide();
|
||||
$form.find('.message-ok,.message-error,.buttons').hide();
|
||||
$form.find('.message-loading').show();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
|
@ -312,6 +313,8 @@
|
|||
success: function(data) {
|
||||
// Handle response
|
||||
console.log(data);
|
||||
$form.find('.buttons').show();
|
||||
$form.find('.message-loading').hide();
|
||||
if (data.status == 'Error') {
|
||||
$form.find('.message-error').html(data.errors.join('\n')).show();
|
||||
} else {
|
||||
|
@ -322,6 +325,7 @@
|
|||
error: function(err) {
|
||||
$form.find('input[type="submit"]').removeClass('disabled');
|
||||
console.error(err.statusText);
|
||||
$form.find('.buttons').show();
|
||||
$form.find('.message-error').show();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -87,6 +87,8 @@
|
|||
'matches' +
|
||||
(projects.length > 3 ? '<i class="float-right">▶▶</i>' : '')
|
||||
);
|
||||
} else {
|
||||
$sm.html('Zero, zilch, zip, nada.')
|
||||
}
|
||||
// Create project cards
|
||||
projects.forEach(function(p) {
|
||||
|
|
|
@ -43,10 +43,10 @@
|
|||
<a href="{{ url_for('public.dashboard', event_id=event.id) }}" class="dropdown-item">
|
||||
Dashboard
|
||||
</a>
|
||||
<!-- <a href="{{ url_for('api.info_event_hackathon_json', event_id=event.id) }}" class="dropdown-item">
|
||||
<a href="{{ url_for('api.info_event_hackathon_json', event_id=event.id) }}" class="dropdown-item">
|
||||
☆ hackathon<b>.json</b>
|
||||
</a> -->
|
||||
<a href="{{ url_for('api.project_list_event_csv', event_id=event.id) }}" class="dropdown-item">
|
||||
</a>
|
||||
<a href="{{ url_for('api.project_list_event_csv', event_id=event.id, moar=True) }}" class="dropdown-item">
|
||||
▼ projects<b>.csv</b>
|
||||
</a>
|
||||
<a href="{{ url_for('api.event_participants_csv', event_id=event.id) }}" class="dropdown-item">
|
||||
|
@ -114,8 +114,8 @@
|
|||
<div class="modal-body">
|
||||
<p class="upload-note mt-0 mb-2">
|
||||
Expects a
|
||||
<b><a href="https://frictionlessdata.io/field-guide/" target="_blank">datapackage.json</a></b>
|
||||
exported from dribdat.
|
||||
<b><a href="https://frictionlessdata.io/field-guide/" target="_blank">datapackage.json</a></b> or
|
||||
<b>projects.csv</b> exported from dribdat.
|
||||
If an event, category or project has the same name on this server as one in the package, it will be skipped.
|
||||
To export event data, tap the drop-down button next to an event listing in the admin.
|
||||
</p>
|
||||
|
@ -138,6 +138,11 @@
|
|||
<input type="radio" name="import" value="full" checked class="form-check-input" id="importCheck3">
|
||||
<label class="form-check-label" for="importCheck3">Include event, projects, users</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<p class="message-loading hidden alert alert-warning">
|
||||
Please wait, loading your data.
|
||||
</p>
|
||||
<p class="message-error hidden alert alert-warning">
|
||||
There was an error. Please check your window log for details.
|
||||
</p>
|
||||
|
@ -145,10 +150,10 @@
|
|||
Import complete.
|
||||
<a href="javascript:location.reload()">Refresh the page</a>, or continue importing events.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<input type="submit" value="Import" class="btn btn-primary">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<div class="buttons">
|
||||
<input type="submit" value="Import" class="btn btn-primary">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -48,6 +48,24 @@
|
|||
{{ render_embed_event(event, event.projects, False) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="an-event-actions text-center">
|
||||
{% if event.lock_resources %}
|
||||
<a href="{{ url_for('public.event_stages', event_id=event.id) }}" class="btn btn-lg btn-warning">
|
||||
<i class="fa fa-life-ring" aria-hidden="true"></i>
|
||||
{{event.name}}
|
||||
</a>
|
||||
{% elif event.has_finished %}
|
||||
<a href="{{ url_for('public.event', event_id=event.id) }}" class="btn btn-light">
|
||||
<i class="fa fa-certificate" aria-hidden="true"></i>
|
||||
Results
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('public.event', event_id=event.id) }}" class="btn btn-light">
|
||||
<i class="fa fa-cubes" aria-hidden="true"></i>
|
||||
Challenges
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div><!-- /.header-content -->
|
||||
</a>
|
||||
|
||||
|
|
|
@ -32,6 +32,4 @@ In the interest of creating a safe and inclusive environment, we strongly encour
|
|||
<label><input type="checkbox"> Make sure your fellow organisers are <u>aware and ready</u> to enforce a Code of Conduct </label><br>
|
||||
<label><input type="checkbox"> Verify compliance during evaluation rounds, when issuing certificates, or other post-event analysis </label><br>
|
||||
|
||||
_Need more help?_ Contact the maintainers at [dribd.at](https://dribd.at/)
|
||||
|
||||
_Disagree with something?_ Please raise [an issue](https://github.com/dribdat/dribdat/issues) or [Edit this page](https://github.com/dribdat/dribdat/blob/main/dribdat/templates/includes/eventstart.md).
|
||||
|
|
|
@ -24,7 +24,14 @@
|
|||
{% if event.location %}
|
||||
<div class="event-location">
|
||||
<i class="fa fa-map"></i>
|
||||
{{ event.location }}</div>
|
||||
{% if event.location_lat and event.location_lon %}
|
||||
<a title="OpenStreetMap" href="https://www.openstreetmap.org/directions?from=&to={{ event.location_lat }}%2C{{ event.location_lon }}" target="_blank">
|
||||
{{ event.location }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ event.location }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,12 +13,7 @@
|
|||
Open web formats (PNG, GIF, JPEG, SVG, WebP) are accepted.
|
||||
<b>Max {{ (config.MAX_CONTENT_LENGTH/(1024*1024))|round(1) }} MB.</b>
|
||||
|
||||
Tip: for slide presentations, embed your presentation or upload a PDF file as your "Demo link" by editing your project.
|
||||
|
||||
<!--<a href="https://hackmd.io/s/how-to-create-slide-deck" target="_blank">HackMD</a>,
|
||||
<a href="https://slides.com" target="_blank">Slides.com</a>, or
|
||||
<a href="https://speakerdeck.com" target="_blank">SpeakerDeck</a>, and
|
||||
put the embed URL into the <b>Demo link</b>.-->
|
||||
<b>Tip:</b> for slides, embed your online presentation, or upload a PDF file as your "Demo link" by editing your project.
|
||||
</p>
|
||||
<center class="preview hidden" id="img-preview">
|
||||
<img src="" height="100">
|
||||
|
@ -85,10 +80,8 @@
|
|||
<sp>GeoJSON</sp>
|
||||
up to {{ (config.MAX_CONTENT_LENGTH/(1024*1024))|round(1) }} MB in size.
|
||||
|
||||
<b>Tip:</b> <a href="https://frictionlessdata.io/field-guide/" target="_blank">Data Packages</a>
|
||||
can be made with a
|
||||
<a href="https://github.com/schoolofdata-ch/datapackage-template" target="_blank">template</a> or
|
||||
<a href="https://create.frictionlessdata.io/" target="_blank">Creator tool</a>.
|
||||
<b>Tip:</b> <a href="https://frictionlessdata.io/" target="_blank">Data Packages</a> can be
|
||||
<a href="https://frictionlessdata.io/projects/#visual-interfaces" target="_blank">made with tools</a> or <a href="https://github.com/schoolofdata-ch/datapackage-template" target="_blank">templates</a>.
|
||||
</p>
|
||||
<div class="input-group" style="margin-top:1em">
|
||||
<div class="input-group-prepend">
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
class="hexagon {{
|
||||
'challenge' if project.is_challenge else 'project'
|
||||
}}{{
|
||||
' stage-%d' % project.progress if project.progress > 0
|
||||
' stage-%d' % project.progress if project.progress
|
||||
}}"
|
||||
{{ render_project_tooltip(project) }}>
|
||||
<div class="hexagontent">
|
||||
|
|
|
@ -38,14 +38,14 @@
|
|||
</div><!-- /organiser -->
|
||||
|
||||
<center>
|
||||
<a href="https://docs.dribdat.cc/usage" class="btn btn-light">📖 User handbook</a>
|
||||
<div class="btn-group m-3">
|
||||
<a href="https://docs.dribdat.cc/usage" class="btn btn-info">User Guide</a>
|
||||
<a href="https://github.com/dribdat/dribdat/issues" class="btn btn-warning">Issues</a>
|
||||
<a href="https://dribdat.cc" class="btn btn-dark">News »</a>
|
||||
{% if config.DRIBDAT_ALLOW_EVENTS %}
|
||||
<a href="{{ url_for('public.event_start') }}" class="btn btn-success"> 📅 Start another Event</a>
|
||||
{% endif %}
|
||||
<a href="https://opencollective.com/dribdat/updates" class="btn btn-dark">News »</a>
|
||||
</div>
|
||||
{% if config.DRIBDAT_ALLOW_EVENTS %}
|
||||
<a href="{{ url_for('public.event_start') }}" class="btn btn-success"> 📅 Start an event</a>
|
||||
{% endif %}
|
||||
</center>
|
||||
|
||||
<hr>
|
||||
|
|
|
@ -135,7 +135,7 @@
|
|||
{% if not current_event and not events_next and not events_past and not events_tips %}
|
||||
<p class="m-5">Thank you for using Dribdat <tt><3 <3 <3</tt>
|
||||
<br>Read <a href="https://docs.dribdat.cc/organiser" target="_blank">the docs</a> or
|
||||
<a href="https://dribd.at/#contact" target="_blank">contact us</a> if you need help.
|
||||
<a href="mailto:dribdat@datalets.ch">contact us</a> if you need help.
|
||||
<h5>Click here to start your first event 🎉 </h5>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -62,21 +62,19 @@
|
|||
</p>
|
||||
{% endif %}
|
||||
|
||||
<form id="loginForm" method="POST" action="{{ url_for('auth.login', next=request.args.get('next')) }}" role="login" class="navbar-form form-inline">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group">
|
||||
{{ form.username(placeholder="Username", class_="form-control") }}
|
||||
{{ form.password(placeholder="Password", class_="form-control") }}
|
||||
<button type="submit" class="btn btn-default btn-primary btn-submit">Enter</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if not config.DRIBDAT_NOT_REGISTER %}
|
||||
<form id="loginForm" method="POST" action="{{ url_for('auth.login', next=request.args.get('next')) }}" role="login" class="navbar-form form-inline">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group">
|
||||
{{ form.username(placeholder="Username", class_="form-control") }}
|
||||
{{ form.password(placeholder="Password", class_="form-control") }}
|
||||
<button type="submit" class="btn btn-default btn-primary btn-submit">Enter</button>
|
||||
</div>
|
||||
</form>
|
||||
<br>
|
||||
<p>Forgot your password or never had one?
|
||||
<a href="{{ url_for('auth.forgot') }}">Help me to get back in</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if not config.DRIBDAT_NOT_REGISTER %}
|
||||
<p class="account-register">
|
||||
<a class="btn btn-warning btn-submit" href="{{ url_for('auth.register', next=request.args.get('next')) }}">Create an account</a>
|
||||
</p>
|
||||
|
|
|
@ -19,6 +19,7 @@ USER_STATUS = {
|
|||
INACTIVE: 'inactive',
|
||||
ACTIVE: 'active',
|
||||
}
|
||||
CLEAR_STATUS_AFTER = 10 # minutes
|
||||
|
||||
# Resource types
|
||||
RESOURCE_TYPES = {
|
||||
|
|
|
@ -5,6 +5,7 @@ from sqlalchemy import Table, or_
|
|||
from sqlalchemy_continuum import make_versioned
|
||||
from sqlalchemy_continuum.plugins import FlaskPlugin
|
||||
from dribdat.user.constants import (
|
||||
CLEAR_STATUS_AFTER,
|
||||
MAX_EXCERPT_LENGTH,
|
||||
PR_CHALLENGE,
|
||||
getProjectPhase,
|
||||
|
@ -175,14 +176,17 @@ class User(UserMixin, PkModel):
|
|||
|
||||
def joined_projects(self, with_challenges=True, limit=-1):
|
||||
"""Retrieve all projects user has joined."""
|
||||
# TODO: slow code!
|
||||
activities = Activity.query.filter_by(
|
||||
user_id=self.id, name='star'
|
||||
).order_by(Activity.timestamp.desc()).all()
|
||||
).order_by(Activity.timestamp.desc())
|
||||
if limit > 0:
|
||||
activities = activities.limit(limit)
|
||||
else:
|
||||
activities = activities.all()
|
||||
projects = []
|
||||
project_ids = []
|
||||
for a in activities:
|
||||
if limit > 0 and len(projects) >= limit: continue
|
||||
if limit > 0 and len(projects) >= limit: break
|
||||
if a.project_id not in project_ids and not a.project.is_hidden:
|
||||
not_challenge = a.project.progress and a.project.progress > 0
|
||||
if with_challenges or not_challenge:
|
||||
|
@ -493,7 +497,7 @@ class Event(PkModel):
|
|||
# Check timeout
|
||||
time_now = dt.datetime.now()
|
||||
# Clear every now and then
|
||||
time_limit = time_now - dt.timedelta(minutes=10)
|
||||
time_limit = time_now - dt.timedelta(minutes=CLEAR_STATUS_AFTER)
|
||||
if dt.datetime.fromtimestamp(status_time) < time_limit:
|
||||
print("Clearing announements")
|
||||
self.status = None
|
||||
|
@ -673,15 +677,16 @@ class Project(PkModel):
|
|||
d['excerpt'] = self.autotext[:MAX_EXCERPT_LENGTH]
|
||||
if len(self.autotext) > MAX_EXCERPT_LENGTH:
|
||||
d['excerpt'] += '...'
|
||||
# Get author
|
||||
if self.user is not None:
|
||||
d['maintainer'] = self.user.username
|
||||
else:
|
||||
d['maintainer'] = ''
|
||||
# Can be empty when embedding
|
||||
if self.event is not None:
|
||||
d['event_url'] = self.event.url
|
||||
d['event_name'] = self.event.name
|
||||
else:
|
||||
d['event_url'] = d['event_name'] = ''
|
||||
# Get categories
|
||||
if self.category is not None:
|
||||
d['category_id'] = self.category.id
|
||||
d['category_name'] = self.category.name
|
||||
|
@ -861,32 +866,50 @@ class Project(PkModel):
|
|||
|
||||
def set_from_data(self, data):
|
||||
"""Update from JSON representation."""
|
||||
if not 'name' in data:
|
||||
raise Exception("Missing project name!")
|
||||
self.name = data['name']
|
||||
self.summary = data['summary']
|
||||
self.hashtag = data['hashtag']
|
||||
self.image_url = data['image_url']
|
||||
self.source_url = data['source_url']
|
||||
self.webpage_url = data['webpage_url']
|
||||
self.autotext_url = data['autotext_url']
|
||||
self.download_url = data['download_url']
|
||||
self.contact_url = data['contact_url']
|
||||
self.logo_color = data['logo_color']
|
||||
self.logo_icon = data['logo_icon']
|
||||
self.longtext = data['longtext']
|
||||
self.autotext = data['autotext']
|
||||
self.score = int(data['score'] or 0)
|
||||
self.progress = int(data['progress'] or 0)
|
||||
if 'summary' in data:
|
||||
self.summary = data['summary']
|
||||
if 'hashtag' in data:
|
||||
self.hashtag = data['hashtag']
|
||||
if 'image_url' in data:
|
||||
self.image_url = data['image_url']
|
||||
if 'source_url' in data:
|
||||
self.source_url = data['source_url']
|
||||
if 'webpage_url' in data:
|
||||
self.webpage_url = data['webpage_url']
|
||||
if 'autotext_url' in data:
|
||||
self.autotext_url = data['autotext_url']
|
||||
if 'download_url' in data:
|
||||
self.download_url = data['download_url']
|
||||
if 'contact_url' in data:
|
||||
self.contact_url = data['contact_url']
|
||||
if 'logo_color' in data:
|
||||
self.logo_color = data['logo_color']
|
||||
if 'logo_icon' in data:
|
||||
self.logo_icon = data['logo_icon']
|
||||
if 'longtext' in data:
|
||||
self.longtext = data['longtext']
|
||||
if 'autotext' in data:
|
||||
self.autotext = data['autotext']
|
||||
if 'score' in data:
|
||||
self.score = int(data['score'] or 0)
|
||||
if 'progress' in data:
|
||||
self.progress = int(data['progress'] or 0)
|
||||
try:
|
||||
if not 'created_at' in data or not 'updated_at' in data:
|
||||
raise ParserError("Date values missing")
|
||||
self.created_at = parse(data['created_at'])
|
||||
self.updated_at = parse(data['updated_at'])
|
||||
except ParserError as ex:
|
||||
# Resetting dates to current time
|
||||
self.created_at = dt.datetime.utcnow()
|
||||
self.updated_at = dt.datetime.utcnow()
|
||||
print(ex)
|
||||
if 'is_autoupdate' in data:
|
||||
self.is_autoupdate = data['is_autoupdate']
|
||||
self.is_autoupdate = bool(data['is_autoupdate'])
|
||||
if 'is_webembed' in data:
|
||||
self.is_webembed = data['is_webembed']
|
||||
self.is_webembed = bool(data['is_webembed'])
|
||||
if 'maintainer' in data:
|
||||
uname = data['maintainer']
|
||||
user = User.query.filter_by(username=uname).first()
|
||||
|
@ -897,6 +920,12 @@ class Project(PkModel):
|
|||
category = Category.query.filter_by(name=cname).first()
|
||||
if category:
|
||||
self.category = category
|
||||
if 'event_id' in data:
|
||||
event = Event.query.filter_by(id=data['event_id']).first()
|
||||
if event: self.event_id = event.id
|
||||
elif 'event_name' in data:
|
||||
event = Event.query.filter_by(name=data['event_name']).first()
|
||||
if event: self.event_id = event.id
|
||||
|
||||
def update_now(self):
|
||||
"""Process data submission."""
|
||||
|
@ -1019,7 +1048,7 @@ class Category(PkModel):
|
|||
ename = data['event_name']
|
||||
evt = Event.query.filter_by(name=ename).first()
|
||||
if evt:
|
||||
self.event = evt
|
||||
self.event_id = evt.id
|
||||
|
||||
def __init__(self, name=None, **kwargs): # noqa: D107
|
||||
if name:
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,16 +1,16 @@
|
|||
alembic==1.12.0 ; python_version >= "3.7" and python_version < "4"
|
||||
alembic==1.12.1 ; python_version >= "3.7" and python_version < "4"
|
||||
aniso8601==9.0.1 ; python_version >= "3.7" and python_version < "4"
|
||||
async-timeout==4.0.3 ; python_version >= "3.7" and python_full_version <= "3.11.2"
|
||||
attrs==23.1.0 ; python_version >= "3.7" and python_version < "4"
|
||||
bcrypt==4.0.1 ; python_version >= "3.7" and python_version < "4"
|
||||
bleach==6.0.0 ; python_version >= "3.7" and python_version < "4"
|
||||
boto3==1.28.44 ; python_version >= "3.7" and python_version < "4"
|
||||
botocore==1.31.44 ; python_version >= "3.7" and python_version < "4"
|
||||
boto3==1.29.7 ; python_version >= "3.7" and python_version < "4"
|
||||
botocore==1.32.7 ; python_version >= "3.7" and python_version < "4"
|
||||
cachelib==0.9.0 ; python_version >= "3.7" and python_version < "4"
|
||||
certifi==2023.7.22 ; python_version >= "3.7" and python_version < "4"
|
||||
certifi==2023.11.17 ; python_version >= "3.7" and python_version < "4"
|
||||
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4"
|
||||
chardet==5.2.0 ; python_version >= "3.7" and python_version < "4"
|
||||
charset-normalizer==3.2.0 ; python_version >= "3.7" and python_version < "4"
|
||||
charset-normalizer==3.3.2 ; python_version >= "3.7" and python_version < "4"
|
||||
click==8.1.7 ; python_version >= "3.7" and python_version < "4"
|
||||
colorama==0.4.6 ; python_version >= "3.7" and python_version < "4" and platform_system == "Windows"
|
||||
cssmin==0.2.0 ; python_version >= "3.7" and python_version < "4"
|
||||
|
@ -18,15 +18,15 @@ cssselect==1.2.0 ; python_version >= "3.7" and python_version < "4"
|
|||
decorator==5.1.1 ; python_version >= "3.7" and python_version < "4"
|
||||
dnspython==2.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
email-validator==1.3.1 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-assets==2.0 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-assets==2.1.0 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-bcrypt==1.0.1 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-caching==2.0.2 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-caching==2.1.0 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-cors==3.0.10 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-dance==6.2.0 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-hashing==1.1 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-login==0.6.2 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-login==0.6.3 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-mailman==0.3.0 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-migrate==4.0.4 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-migrate==4.0.5 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-misaka==1.0.0 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-sqlalchemy==3.0.5 ; python_version >= "3.7" and python_version < "4"
|
||||
flask-talisman==1.1.0 ; python_version >= "3.7" and python_version < "4"
|
||||
|
@ -38,10 +38,10 @@ gevent==22.10.2 ; python_version >= "3.7" and python_version < "4"
|
|||
graphene==3.3 ; python_version >= "3.7" and python_version < "4"
|
||||
graphql-core==3.2.3 ; python_version >= "3.7" and python_version < "4"
|
||||
graphql-relay==3.2.0 ; python_version >= "3.7" and python_version < "4"
|
||||
greenlet==2.0.2 ; python_version >= "3.7" and python_version < "4" and (platform_machine == "win32" or platform_machine == "WIN32" or platform_machine == "AMD64" or platform_machine == "amd64" or platform_machine == "x86_64" or platform_machine == "ppc64le" or platform_machine == "aarch64" or platform_python_implementation == "CPython")
|
||||
greenlet==3.0.1 ; python_version >= "3.7" and python_version < "4" and (platform_machine == "win32" or platform_machine == "WIN32" or platform_machine == "AMD64" or platform_machine == "amd64" or platform_machine == "x86_64" or platform_machine == "ppc64le" or platform_machine == "aarch64" or platform_python_implementation == "CPython")
|
||||
gunicorn[gevent]==20.1.0 ; python_version >= "3.7" and python_version < "4"
|
||||
hiredis==2.2.3 ; python_version >= "3.7" and python_version < "4"
|
||||
idna==3.4 ; python_version >= "3.7" and python_version < "4"
|
||||
idna==3.6 ; python_version >= "3.7" and python_version < "4"
|
||||
importlib-metadata==4.2.0 ; python_version >= "3.7" and python_version < "3.10"
|
||||
importlib-resources==5.12.0 ; python_version >= "3.7" and python_version < "3.9"
|
||||
isodate==0.6.1 ; python_version >= "3.7" and python_version < "4"
|
||||
|
@ -53,18 +53,18 @@ jsonschema==4.17.3 ; python_version >= "3.7" and python_version < "4"
|
|||
lxml==4.9.3 ; python_version >= "3.7" and python_version < "4"
|
||||
mako==1.2.4 ; python_version >= "3.7" and python_version < "4"
|
||||
markdown-it-py==2.2.0 ; python_version >= "3.7" and python_version < "4"
|
||||
marko==2.0.0 ; python_version >= "3.7" and python_version < "4"
|
||||
marko==2.0.2 ; python_version >= "3.7" and python_version < "4"
|
||||
markupsafe==2.1.3 ; python_version >= "3.7" and python_version < "4"
|
||||
mdurl==0.1.2 ; python_version >= "3.7" and python_version < "4"
|
||||
micawber==0.5.5 ; python_version >= "3.7" and python_version < "4"
|
||||
misaka==2.1.1 ; python_version >= "3.7" and python_version < "4"
|
||||
mkdocs-material-extensions==1.1.1 ; python_version >= "3.7" and python_version < "4"
|
||||
mkdocs-material-extensions==1.2 ; python_version >= "3.7" and python_version < "4"
|
||||
oauthlib==3.2.2 ; python_version >= "3.7" and python_version < "4"
|
||||
petl==1.7.14 ; python_version >= "3.7" and python_version < "4"
|
||||
pkgutil-resolve-name==1.3.10 ; python_version >= "3.7" and python_version < "3.9"
|
||||
psycopg2-binary==2.9.7 ; python_version >= "3.7" and python_version < "4"
|
||||
psycopg2-binary==2.9.9 ; python_version >= "3.7" and python_version < "4"
|
||||
pycparser==2.21 ; python_version >= "3.7" and python_version < "4"
|
||||
pygments==2.16.1 ; python_version >= "3.7" and python_version < "4"
|
||||
pygments==2.17.2 ; python_version >= "3.7" and python_version < "4"
|
||||
pyquery==2.0.0 ; python_version >= "3.7" and python_version < "4"
|
||||
pyrsistent==0.19.3 ; python_version >= "3.7" and python_version < "4"
|
||||
pystache==0.6.4 ; python_version >= "3.7" and python_version < "4"
|
||||
|
@ -77,21 +77,21 @@ redis==4.6.0 ; python_version >= "3.7" and python_version < "4"
|
|||
requests-oauthlib==1.3.1 ; python_version >= "3.7" and python_version < "4"
|
||||
requests==2.31.0 ; python_version >= "3.7" and python_version < "4"
|
||||
rfc3986==2.0.0 ; python_version >= "3.7" and python_version < "4"
|
||||
rich==13.5.2 ; python_version >= "3.7" and python_version < "4"
|
||||
s3transfer==0.6.2 ; python_version >= "3.7" and python_version < "4"
|
||||
rich==13.7.0 ; python_version >= "3.7" and python_version < "4"
|
||||
s3transfer==0.8.0 ; python_version >= "3.7" and python_version < "4"
|
||||
setuptools==68.0.0 ; python_version >= "3.7" and python_version < "4"
|
||||
shellingham==1.5.3 ; python_version >= "3.7" and python_version < "4"
|
||||
shellingham==1.5.4 ; python_version >= "3.7" and python_version < "4"
|
||||
simpleeval==0.9.13 ; python_version >= "3.7" and python_version < "4"
|
||||
six==1.16.0 ; python_version >= "3.7" and python_version < "4"
|
||||
sqlalchemy-continuum==1.4.0 ; python_version >= "3.7" and python_version < "4"
|
||||
sqlalchemy-utils==0.41.1 ; python_version >= "3.7" and python_version < "4"
|
||||
sqlalchemy==1.4.49 ; python_version >= "3.7" and python_version < "4"
|
||||
sqlalchemy==1.4.50 ; python_version >= "3.7" and python_version < "4"
|
||||
stringcase==1.2.0 ; python_version >= "3.7" and python_version < "4"
|
||||
tabulate==0.9.0 ; python_version >= "3.7" and python_version < "4"
|
||||
text-unidecode==1.3 ; python_version >= "3.7" and python_version < "4"
|
||||
typer[all]==0.9.0 ; python_version >= "3.7" and python_version < "4"
|
||||
typing-extensions==4.7.1 ; python_version >= "3.7" and python_version < "4"
|
||||
urllib3==1.26.16 ; python_version >= "3.7" and python_version < "4"
|
||||
urllib3==1.26.18 ; python_version >= "3.7" and python_version < "4"
|
||||
urlobject==2.4.3 ; python_version >= "3.7" and python_version < "4"
|
||||
validators==0.20.0 ; python_version >= "3.7" and python_version < "4"
|
||||
webassets==2.0 ; python_version >= "3.7" and python_version < "4"
|
||||
|
@ -101,4 +101,4 @@ whitenoise==5.3.0 ; python_version >= "3.7" and python_version < "4"
|
|||
wtforms==3.0.1 ; python_version >= "3.7" and python_version < "4"
|
||||
zipp==3.15.0 ; python_version >= "3.7" and python_version < "3.10"
|
||||
zope-event==5.0 ; python_version >= "3.7" and python_version < "4"
|
||||
zope-interface==6.0 ; python_version >= "3.7" and python_version < "4"
|
||||
zope-interface==6.1 ; python_version >= "3.7" and python_version < "4"
|
||||
|
|
|
@ -152,6 +152,9 @@ class TestRegistering:
|
|||
assert res.status_code == 200
|
||||
# A new user was created
|
||||
assert User.query.count() == old_count + 1
|
||||
# Check user can create an event
|
||||
view_html = testapp.get('/event/start')
|
||||
assert 'Get started' in view_html
|
||||
|
||||
class TestActivation:
|
||||
"""Activate a user to interact with projects."""
|
||||
|
|
|
@ -2,9 +2,18 @@
|
|||
"""Dribdat data import export tests."""
|
||||
|
||||
from dribdat.user.models import Event, Project, Activity, Role
|
||||
from dribdat.apipackage import import_event_package, event_to_data_package
|
||||
from dribdat.aggregation import ProjectActivity
|
||||
from dribdat.apiutils import get_schema_for_user_projects
|
||||
from dribdat.apiutils import (
|
||||
event_upload_configuration,
|
||||
get_schema_for_user_projects,
|
||||
get_project_list,
|
||||
gen_csv,
|
||||
)
|
||||
from dribdat.apipackage import (
|
||||
event_to_data_package,
|
||||
import_event_package,
|
||||
import_project_data,
|
||||
)
|
||||
from .factories import UserFactory
|
||||
|
||||
|
||||
|
@ -88,3 +97,35 @@ class TestImport:
|
|||
proj2.delete()
|
||||
event.delete()
|
||||
user.delete()
|
||||
|
||||
def test_import_export(self, project, testapp):
|
||||
"""Create an export sample."""
|
||||
event = Event(name="Test Event", summary="Just testin")
|
||||
event.save()
|
||||
user = UserFactory(username="Test Author")
|
||||
user.save()
|
||||
for p in range(1, 6):
|
||||
proj = Project(name="Test Project %d" % p)
|
||||
proj.event = event
|
||||
proj.user = user
|
||||
proj.progress = p * 10
|
||||
proj.save()
|
||||
testlist = get_project_list(event.id, 'testhost', True)
|
||||
assert len(testlist) == 5
|
||||
testcsv = gen_csv(testlist)
|
||||
assert testcsv.startswith('"id",')
|
||||
testimport = import_project_data(testlist, True, event)
|
||||
assert len(testimport) == 5
|
||||
assert 'Test Project' in testimport[0]['name']
|
||||
|
||||
def test_configuration(self):
|
||||
d,a,s = event_upload_configuration()
|
||||
assert d
|
||||
assert not a
|
||||
assert s == 'Preview'
|
||||
d,a,s = event_upload_configuration('basic')
|
||||
assert not d
|
||||
assert not a
|
||||
d,a,s = event_upload_configuration('full')
|
||||
assert not d
|
||||
assert a
|
||||
|
|
|
@ -204,6 +204,19 @@ class TestProject:
|
|||
ProjectActivity(project, 'unstar', user2)
|
||||
project.update_now()
|
||||
assert project.score == 1
|
||||
|
||||
def test_project_from_data(self):
|
||||
project = ProjectFactory()
|
||||
event = EventFactory()
|
||||
project.event = event
|
||||
project.save()
|
||||
testdata = project.data
|
||||
testdata['name'] = 'Testme'
|
||||
project2 = ProjectFactory()
|
||||
project2.set_from_data(testdata)
|
||||
project2.save()
|
||||
assert project2.name == 'Testme'
|
||||
assert project2.event == project.event
|
||||
|
||||
# @pytest.mark.usefixtures('db')
|
||||
# class TestResource:
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Dribdat view tests using WebTest.
|
||||
|
||||
See: http://webtest.readthedocs.org/
|
||||
"""
|
||||
from .factories import ProjectFactory, EventFactory, UserFactory
|
||||
from dribdat.public import views
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TestViews:
|
||||
"""Home views."""
|
||||
|
||||
def test_public_views(self, event, testapp):
|
||||
"""Check stage progression."""
|
||||
event = EventFactory()
|
||||
event.status = '99999999999;Hello Status'
|
||||
event.is_current = True
|
||||
event.save()
|
||||
project = ProjectFactory()
|
||||
project.event = event
|
||||
project.save()
|
||||
|
||||
# Check the dashboard status
|
||||
view_html = testapp.get('/dashboard/')
|
||||
assert 'Hello Status' in view_html
|
||||
|
||||
# Test the history view
|
||||
event2 = EventFactory()
|
||||
event2.name = 'Special Event'
|
||||
event2.ends_at = datetime(1970, 1, 1)
|
||||
event2.save()
|
||||
view_html = testapp.get('/history')
|
||||
assert 'Special Event' in view_html
|
||||
|
||||
# Test the stages view
|
||||
view_html = testapp.get('/event/%d/stages' % event.id)
|
||||
assert 'Stages' in view_html
|
||||
|
||||
# Test the print view
|
||||
view_html = testapp.get('/event/%d/print' % event.id)
|
||||
assert 'All projects' in view_html
|
||||
|
||||
# Test the dribs view
|
||||
view_html = testapp.get('/dribs')
|
||||
assert 'in small amounts' in view_html
|
||||
|
Loading…
Reference in New Issue