Merge pull request #369 from loleg/main

Event home and fall updates
This commit is contained in:
Oleg Lavrovsky 2023-11-28 10:44:39 +01:00 committed by GitHub
commit 911d936b02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 732 additions and 501 deletions

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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:

View File

@ -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; }

View File

@ -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();
}
});

View File

@ -87,6 +87,8 @@
'matches' +
(projects.length > 3 ? '<i class="float-right">&#9654;&#9654;</i>' : '')
);
} else {
$sm.html('Zero, zilch, zip, nada.')
}
// Create project cards
projects.forEach(function(p) {

View File

@ -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">
&#9734; 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">
&#9660; 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>

View File

@ -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>

View File

@ -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).

View File

@ -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>

View File

@ -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">

View File

@ -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">

View File

@ -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 &raquo;</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 &raquo;</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>

View File

@ -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>&lt;3 &lt;3 &lt;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 %}

View File

@ -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>

View File

@ -19,6 +19,7 @@ USER_STATUS = {
INACTIVE: 'inactive',
ACTIVE: 'active',
}
CLEAR_STATUS_AFTER = 10 # minutes
# Resource types
RESOURCE_TYPES = {

View File

@ -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:

787
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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."""

View File

@ -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

View File

@ -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:

47
tests/test_views.py Normal file
View File

@ -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