def test_create_history_model_with_one_to_one_field_to_char_field(self):
     records = HistoricalRecords()
     records.module = Bookcase.__module__
     try:
         records.create_history_model(Bookcase)
     except:
         self.fail("SimpleHistory should handle foreign keys to one to one"
                   "fields to char fields without throwing an exception.")
 def test_create_history_model_with_one_to_one_field_to_integer_field(self):
     records = HistoricalRecords()
     records.module = AdminProfile.__module__
     try:
         records.create_history_model(AdminProfile)
     except:
         self.fail("SimpleHistory should handle foreign keys to one to one"
                   "fields to integer fields without throwing an exception")
 def test_create_history_model_with_multiple_one_to_ones(self):
     records = HistoricalRecords()
     records.module = MultiOneToOne.__module__
     try:
         records.create_history_model(MultiOneToOne)
     except:
         self.fail("SimpleHistory should handle foreign keys to one to one"
                   "fields to one to one fields without throwing an "
                   "exception.")
Beispiel #4
0
class OrderDiscount(AbstractOrderDiscount):
    history = HistoricalRecords()
class CourseOverview(TimeStampedModel):
    """
    Model for storing and caching basic information about a course.

    This model contains basic course metadata such as an ID, display name,
    image URL, and any other information that would be necessary to display
    a course as part of:
        user dashboard (enrolled courses)
        course catalog (courses to enroll in)
        course about (meta data about the course)

    .. no_pii:
    """

    class Meta(object):
        app_label = 'course_overviews'

    # IMPORTANT: Bump this whenever you modify this model and/or add a migration.
    VERSION = 11  # this one goes to eleven

    # Cache entry versioning.
    version = IntegerField()

    # Course identification
    id = CourseKeyField(db_index=True, primary_key=True, max_length=255)
    _location = UsageKeyField(max_length=255)
    org = TextField(max_length=255, default=u'outdated_entry')
    display_name = TextField(null=True)
    display_number_with_default = TextField()
    display_org_with_default = TextField()

    # Start/end dates
    # TODO Remove 'start' & 'end' in removing field in column renaming, DE-1822
    start = DateTimeField(null=True)
    end = DateTimeField(null=True)
    start_date = DateTimeField(null=True)
    end_date = DateTimeField(null=True)
    advertised_start = TextField(null=True)
    announcement = DateTimeField(null=True)

    # URLs
    course_image_url = TextField()
    social_sharing_url = TextField(null=True)
    end_of_course_survey_url = TextField(null=True)

    # Certification data
    certificates_display_behavior = TextField(null=True)
    certificates_show_before_end = BooleanField(default=False)
    cert_html_view_enabled = BooleanField(default=False)
    has_any_active_web_certificate = BooleanField(default=False)
    cert_name_short = TextField()
    cert_name_long = TextField()
    certificate_available_date = DateTimeField(default=None, null=True)

    # Grading
    lowest_passing_grade = DecimalField(max_digits=5, decimal_places=2, null=True)

    # Access parameters
    days_early_for_beta = FloatField(null=True)
    mobile_available = BooleanField(default=False)
    visible_to_staff_only = BooleanField(default=False)
    _pre_requisite_courses_json = TextField()  # JSON representation of list of CourseKey strings

    # Enrollment details
    enrollment_start = DateTimeField(null=True)
    enrollment_end = DateTimeField(null=True)
    enrollment_domain = TextField(null=True)
    invitation_only = BooleanField(default=False)
    max_student_enrollments_allowed = IntegerField(null=True)

    # Catalog information
    catalog_visibility = TextField(null=True)
    short_description = TextField(null=True)
    course_video_url = TextField(null=True)
    effort = TextField(null=True)
    self_paced = BooleanField(default=False)
    marketing_url = TextField(null=True)
    eligible_for_financial_aid = BooleanField(default=True)

    language = TextField(null=True)

    history = HistoricalRecords()

    @classmethod
    def _create_or_update(cls, course):
        """
        Creates or updates a CourseOverview object from a CourseDescriptor.

        Does not touch the database, simply constructs and returns an overview
        from the given course.

        Arguments:
            course (CourseDescriptor): any course descriptor object

        Returns:
            CourseOverview: created or updated overview extracted from the given course
        """
        from lms.djangoapps.certificates.api import get_active_web_certificate
        from openedx.core.lib.courses import course_image_url

        # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806.
        # If the course has a malformed grading policy such that
        # course._grading_policy['GRADE_CUTOFFS'] = {}, then
        # course.lowest_passing_grade will raise a ValueError.
        # Work around this for now by defaulting to None.
        try:
            lowest_passing_grade = course.lowest_passing_grade
        except ValueError:
            lowest_passing_grade = None

        display_name = course.display_name
        start = course.start
        end = course.end
        max_student_enrollments_allowed = course.max_student_enrollments_allowed
        if isinstance(course.id, CCXLocator):
            from lms.djangoapps.ccx.utils import get_ccx_from_ccx_locator
            ccx = get_ccx_from_ccx_locator(course.id)
            display_name = ccx.display_name
            start = ccx.start
            end = ccx.due
            max_student_enrollments_allowed = ccx.max_student_enrollments_allowed

        course_overview = cls.objects.filter(id=course.id)
        if course_overview.exists():
            log.info(u'Updating course overview for %s.', six.text_type(course.id))
            course_overview = course_overview.first()
            # MySQL ignores casing, but CourseKey doesn't. To prevent multiple
            # courses with different cased keys from overriding each other, we'll
            # check for equality here in python.
            if course_overview.id != course.id:
                raise CourseOverviewCaseMismatchException(course_overview.id, course.id)
        else:
            log.info(u'Creating course overview for %s.', six.text_type(course.id))
            course_overview = cls()

        course_overview.version = cls.VERSION
        course_overview.id = course.id
        course_overview._location = course.location
        course_overview.org = course.location.org
        course_overview.display_name = display_name
        course_overview.display_number_with_default = course.display_number_with_default
        course_overview.display_org_with_default = course.display_org_with_default

        course_overview.start = start
        # Add writes to new fields 'start_date' & 'end_date'.
        course_overview.start_date = start
        course_overview.end = end
        course_overview.end_date = end
        course_overview.advertised_start = course.advertised_start
        course_overview.announcement = course.announcement

        course_overview.course_image_url = course_image_url(course)
        course_overview.social_sharing_url = course.social_sharing_url

        course_overview.certificates_display_behavior = course.certificates_display_behavior
        course_overview.certificates_show_before_end = course.certificates_show_before_end
        course_overview.cert_html_view_enabled = course.cert_html_view_enabled
        course_overview.has_any_active_web_certificate = (get_active_web_certificate(course) is not None)
        course_overview.cert_name_short = course.cert_name_short
        course_overview.cert_name_long = course.cert_name_long
        course_overview.certificate_available_date = course.certificate_available_date
        course_overview.lowest_passing_grade = lowest_passing_grade
        course_overview.end_of_course_survey_url = course.end_of_course_survey_url

        course_overview.days_early_for_beta = course.days_early_for_beta
        course_overview.mobile_available = course.mobile_available
        course_overview.visible_to_staff_only = course.visible_to_staff_only
        course_overview._pre_requisite_courses_json = json.dumps(course.pre_requisite_courses)

        course_overview.enrollment_start = course.enrollment_start
        course_overview.enrollment_end = course.enrollment_end
        course_overview.enrollment_domain = course.enrollment_domain
        course_overview.invitation_only = course.invitation_only
        course_overview.max_student_enrollments_allowed = max_student_enrollments_allowed

        course_overview.catalog_visibility = course.catalog_visibility
        course_overview.short_description = CourseDetails.fetch_about_attribute(course.id, 'short_description')
        course_overview.effort = CourseDetails.fetch_about_attribute(course.id, 'effort')
        course_overview.course_video_url = CourseDetails.fetch_video_url(course.id)
        course_overview.self_paced = course.self_paced

        if not CatalogIntegration.is_enabled():
            course_overview.language = course.language

        return course_overview

    @classmethod
    def load_from_module_store(cls, course_id):
        """
        Load a CourseDescriptor, create or update a CourseOverview from it, cache the
        overview, and return it.

        Arguments:
            course_id (CourseKey): the ID of the course overview to be loaded.

        Returns:
            CourseOverview: overview of the requested course.

        Raises:
            - CourseOverview.DoesNotExist if the course specified by course_id
                was not found.
            - IOError if some other error occurs while trying to load the
                course from the module store.
        """
        log.info(
            "Attempting to load CourseOverview for course %s from modulestore.",
            course_id,
        )
        store = modulestore()
        with store.bulk_operations(course_id):
            course = store.get_course(course_id)
            if isinstance(course, CourseDescriptor):
                try:
                    course_overview = cls._create_or_update(course)
                    with transaction.atomic():
                        course_overview.save()
                        # Remove and recreate all the course tabs
                        CourseOverviewTab.objects.filter(course_overview=course_overview).delete()
                        CourseOverviewTab.objects.bulk_create([
                            CourseOverviewTab(
                                tab_id=tab.tab_id,
                                type=tab.type,
                                name=tab.name,
                                course_staff_only=tab.course_staff_only,
                                url_slug=tab.get('url_slug'),
                                link=tab.get('link'),
                                is_hidden=tab.get('is_hidden', False),
                                course_overview=course_overview)
                            for tab in course.tabs
                        ])
                        # Remove and recreate course images
                        CourseOverviewImageSet.objects.filter(course_overview=course_overview).delete()
                        CourseOverviewImageSet.create(course_overview, course)

                except IntegrityError:
                    # There is a rare race condition that will occur if
                    # CourseOverview.get_from_id is called while a
                    # another identical overview is already in the process
                    # of being created.
                    # One of the overviews will be saved normally, while the
                    # other one will cause an IntegrityError because it tries
                    # to save a duplicate.
                    # (see: https://openedx.atlassian.net/browse/TNL-2854).
                    log.info(
                        "Multiple CourseOverviews for course %s requested "
                        "simultaneously; will only save one.",
                        course_id,
                    )
                except Exception:
                    log.exception(
                        "Saving CourseOverview for course %s failed with "
                        "unexpected exception!",
                        course_id,
                    )
                    raise

                return course_overview
            elif course is not None:
                raise IOError(
                    "Error while loading CourseOverview for course {} "
                    "from the module store: {}",
                    six.text_type(course_id),
                    course.error_msg if isinstance(course, ErrorDescriptor) else six.text_type(course)
                )
            else:
                log.info(
                    "Could not create CourseOverview for non-existent course: %s",
                    course_id,
                )
                raise cls.DoesNotExist()

    @classmethod
    def course_exists(cls, course_id):
        """
        Check whether a course run exists (in CourseOverviews _or_ modulestore).

        Checks the CourseOverview table first.
        If it is not there, check the modulestore.
        Equivalent to, but more efficient than:
            bool(CourseOverview.get_from_id(course_id))

        Arguments:
            course_id (CourseKey)

        Returns: bool
        """
        if cls.objects.filter(id=course_id).exists():
            return True
        return modulestore().has_course(course_id)

    @classmethod
    @request_cached('course_overview')
    def get_from_id(cls, course_id):
        """
        Load a CourseOverview object for a given course ID.

        First, we try to load the CourseOverview from the database. If it
        doesn't exist, we load the entire course from the modulestore, create a
        CourseOverview object from it, and then cache it in the database for
        future use.

        Arguments:
            course_id (CourseKey): the ID of the course overview to be loaded.

        Returns:
            CourseOverview: overview of the requested course.

        Raises:
            - CourseOverview.DoesNotExist if the course specified by course_id
                was not found.
            - IOError if some other error occurs while trying to load the
                course from the module store.
        """
        try:
            course_overview = cls.objects.select_related('image_set').get(id=course_id)
            if course_overview.version < cls.VERSION:
                # Reload the overview from the modulestore to update the version
                course_overview = cls.load_from_module_store(course_id)
        except cls.DoesNotExist:
            course_overview = None

        # Regenerate the thumbnail images if they're missing (either because
        # they were never generated, or because they were flushed out after
        # a change to CourseOverviewImageConfig.
        if course_overview and not hasattr(course_overview, 'image_set'):
            CourseOverviewImageSet.create(course_overview)

        return course_overview or cls.load_from_module_store(course_id)

    @classmethod
    def get_from_ids(cls, course_ids):
        """
        Return a dict mapping course_ids to CourseOverviews.

        Tries to select all CourseOverviews in one query,
        then fetches remaining (uncached) overviews from the modulestore.

        Course IDs for non-existant courses will map to None.

        Arguments:
            course_ids (iterable[CourseKey])

        Returns: dict[CourseKey, CourseOverview|None]
        """
        overviews = {
            overview.id: overview
            for overview in cls.objects.select_related('image_set').filter(
                id__in=course_ids,
                version__gte=cls.VERSION
            )
        }
        for course_id in course_ids:
            if course_id not in overviews:
                try:
                    overviews[course_id] = cls.load_from_module_store(course_id)
                except CourseOverview.DoesNotExist:
                    overviews[course_id] = None
        return overviews

    def clean_id(self, padding_char='='):
        """
        Returns a unique deterministic base32-encoded ID for the course.

        Arguments:
            padding_char (str): Character used for padding at end of base-32
                                -encoded string, defaulting to '='
        """
        return course_metadata_utils.clean_course_key(self.location.course_key, padding_char)

    @property
    def location(self):
        """
        Returns the UsageKey of this course.

        UsageKeyField has a strange behavior where it fails to parse the "run"
        of a course out of the serialized form of a Mongo Draft UsageKey. This
        method is a wrapper around _location attribute that fixes the problem
        by calling map_into_course, which restores the run attribute.
        """
        if self._location.run is None:
            self._location = self._location.map_into_course(self.id)
        return self._location

    @property
    def number(self):
        """
        Returns this course's number.

        This is a "number" in the sense of the "course numbers" that you see at
        lots of universities. For example, given a course
        "Intro to Computer Science" with the course key "edX/CS-101/2014", the
        course number would be "CS-101"
        """
        return course_metadata_utils.number_for_course_location(self.location)

    @property
    def url_name(self):
        """
        Returns this course's URL name.
        """
        return block_metadata_utils.url_name_for_block(self)

    @property
    def display_name_with_default(self):
        """
        Return reasonable display name for the course.
        """
        return block_metadata_utils.display_name_with_default(self)

    @property
    def display_name_with_default_escaped(self):
        """
        DEPRECATED: use display_name_with_default

        Return html escaped reasonable display name for the course.

        Note: This newly introduced method should not be used.  It was only
        introduced to enable a quick search/replace and the ability to slowly
        migrate and test switching to display_name_with_default, which is no
        longer escaped.
        """
        # pylint: disable=line-too-long
        return block_metadata_utils.display_name_with_default_escaped(self)  # xss-lint: disable=python-deprecated-display-name

    @property
    def dashboard_start_display(self):
        """
         Return start date to diplay on learner's dashboard, preferably `Course Advertised Start`
        """
        return self.advertised_start or self.start

    def has_started(self):
        """
        Returns whether the the course has started.
        """
        return course_metadata_utils.has_course_started(self.start)

    def has_ended(self):
        """
        Returns whether the course has ended.
        """
        return course_metadata_utils.has_course_ended(self.end)

    def has_marketing_url(self):
        """
        Returns whether the course has marketing url.
        """
        return settings.FEATURES.get('ENABLE_MKTG_SITE') and bool(self.marketing_url)

    def has_social_sharing_url(self):
        """
        Returns whether the course has social sharing url.
        """
        is_social_sharing_enabled = getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get('CUSTOM_COURSE_URLS')
        return is_social_sharing_enabled and bool(self.social_sharing_url)

    def starts_within(self, days):
        """
        Returns True if the course starts with-in given number of days otherwise returns False.
        """
        return course_metadata_utils.course_starts_within(self.start, days)

    @property
    def start_date_is_still_default(self):
        """
        Checks if the start date set for the course is still default, i.e.
        .start has not been modified, and .advertised_start has not been set.
        """
        return course_metadata_utils.course_start_date_is_default(
            self.start,
            self.advertised_start,
        )

    @property
    def sorting_score(self):
        """
        Returns a tuple that can be used to sort the courses according
        the how "new" they are. The "newness" score is computed using a
        heuristic that takes into account the announcement and
        (advertised) start dates of the course if available.

        The lower the number the "newer" the course.
        """
        return course_metadata_utils.sorting_score(self.start, self.advertised_start, self.announcement)

    @property
    def start_type(self):
        """
        Returns the type of the course's 'start' field.
        """
        if self.advertised_start:
            return u'string'
        elif self.start != DEFAULT_START_DATE:
            return u'timestamp'
        else:
            return u'empty'

    @property
    def start_display(self):
        """
        Returns the display value for the course's start date.
        """
        if self.advertised_start:
            return self.advertised_start
        elif self.start != DEFAULT_START_DATE:
            return defaultfilters.date(self.start, "DATE_FORMAT")
        else:
            return None

    def may_certify(self):
        """
        Returns whether it is acceptable to show the student a certificate
        download link.
        """
        return course_metadata_utils.may_certify_for_course(
            self.certificates_display_behavior,
            self.certificates_show_before_end,
            self.has_ended(),
            self.certificate_available_date,
            self.self_paced
        )

    @property
    def pre_requisite_courses(self):
        """
        Returns a list of ID strings for this course's prerequisite courses.
        """
        return json.loads(self._pre_requisite_courses_json)

    @pre_requisite_courses.setter
    def pre_requisite_courses(self, value):
        """
        Django requires there be a setter for this, but it is not
        necessary for the way we currently use it. Due to the way
        CourseOverviews are constructed raising errors here will
        cause a lot of issues. These should not be mutable after
        construction, so for now we just eat this.
        """
        pass

    @classmethod
    def update_select_courses(cls, course_keys, force_update=False):
        """
        A side-effecting method that updates CourseOverview objects for
        the given course_keys.

        Arguments:
            course_keys (list[CourseKey]): Identifies for which courses to
                return CourseOverview objects.
            force_update (boolean): Optional parameter that indicates
                whether the requested CourseOverview objects should be
                forcefully updated (i.e., re-synched with the modulestore).
        """
        log.info(u'Generating course overview for %d courses.', len(course_keys))
        log.debug(u'Generating course overview(s) for the following courses: %s', course_keys)

        action = CourseOverview.load_from_module_store if force_update else CourseOverview.get_from_id

        for course_key in course_keys:
            try:
                action(course_key)
            except Exception as ex:  # pylint: disable=broad-except
                log.exception(
                    u'An error occurred while generating course overview for %s: %s',
                    six.text_type(course_key),
                    text_type(ex),
                )

        log.info('Finished generating course overviews.')

    @classmethod
    def get_all_courses(cls, orgs=None, filter_=None):
        """
        Return a queryset containing all CourseOverview objects in the database.

        Arguments:
            orgs (list[string]): Optional parameter that allows case-insensitive
                filtering by organization.
            filter_ (dict): Optional parameter that allows custom filtering.
        """
        # Note: If a newly created course is not returned in this QueryList,
        # make sure the "publish" signal was emitted when the course was
        # created. For tests using CourseFactory, use emit_signals=True.
        course_overviews = CourseOverview.objects.all()

        if orgs:
            # In rare cases, courses belonging to the same org may be accidentally assigned
            # an org code with a different casing (e.g., Harvardx as opposed to HarvardX).
            # Case-insensitive matching allows us to deal with this kind of dirty data.
            org_filter = Q()  # Avoiding the `reduce()` for more readability, so a no-op filter starter is needed.
            for org in orgs:
                org_filter |= Q(org__iexact=org)
            course_overviews = course_overviews.filter(org_filter)

        if filter_:
            course_overviews = course_overviews.filter(**filter_)

        return course_overviews

    @classmethod
    def get_all_course_keys(cls):
        """
        Returns all course keys from course overviews.
        """
        return CourseOverview.objects.values_list('id', flat=True)

    def is_discussion_tab_enabled(self):
        """
        Returns True if course has discussion tab and is enabled
        """
        tabs = self.tab_set.all()
        # creates circular import; hence explicitly referenced is_discussion_enabled
        for tab in tabs:
            if tab.tab_id == "discussion" and django_comment_client.utils.is_discussion_enabled(self.id):
                return True
        return False

    @property
    def tabs(self):
        """
        Returns an iterator of CourseTabs.
        """
        for tab_dict in self.tab_set.all().values():
            tab = CourseTab.from_json(tab_dict)
            if tab is None:
                log.warning("Can't instantiate CourseTab from %r", tab_dict)
            else:
                yield tab

    @property
    def image_urls(self):
        """
        Return a dict with all known URLs for this course image.

        Current resolutions are:
          raw = original upload from the user
          small = thumbnail with dimensions CourseOverviewImageConfig.current().small
          large = thumbnail with dimensions CourseOverviewImageConfig.current().large

        If no thumbnails exist, the raw (originally uploaded) image will be
        returned for all resolutions.
        """
        # This is either the raw image that the course team uploaded, or the
        # settings.DEFAULT_COURSE_ABOUT_IMAGE_URL if they didn't specify one.
        raw_image_url = self.course_image_url

        # Default all sizes to return the raw image if there is no
        # CourseOverviewImageSet associated with this CourseOverview. This can
        # happen because we're disabled via CourseOverviewImageConfig.
        urls = {
            'raw': raw_image_url,
            'small': raw_image_url,
            'large': raw_image_url,
        }

        # If we do have a CourseOverviewImageSet, we still default to the raw
        # images if our thumbnails are blank (might indicate that there was a
        # processing error of some sort while trying to generate thumbnails).
        if hasattr(self, 'image_set') and CourseOverviewImageConfig.current().enabled:
            urls['small'] = self.image_set.small_url or raw_image_url
            urls['large'] = self.image_set.large_url or raw_image_url

        return self.apply_cdn_to_urls(urls)

    @property
    def pacing(self):
        """ Returns the pacing for the course.

        Potential values:
            self: Self-paced courses
            instructor: Instructor-led courses
        """
        return 'self' if self.self_paced else 'instructor'

    @property
    def closest_released_language(self):
        """
        Returns the language code that most closely matches this course' language and is fully
        supported by the LMS, or None if there are no fully supported languages that
        match the target.
        """
        return get_closest_released_language(self.language) if self.language else None

    def apply_cdn_to_urls(self, image_urls):
        """
        Given a dict of resolutions -> urls, return a copy with CDN applied.

        If CDN does not exist or is disabled, just returns the original. The
        URLs that we store in CourseOverviewImageSet are all already top level
        paths, so we don't need to go through the /static remapping magic that
        happens with other course assets. We just need to add the CDN server if
        appropriate.
        """
        cdn_config = AssetBaseUrlConfig.current()
        if not cdn_config.enabled:
            return image_urls

        base_url = cdn_config.base_url

        return {
            resolution: self._apply_cdn_to_url(url, base_url)
            for resolution, url in image_urls.items()
        }

    def _apply_cdn_to_url(self, url, base_url):
        """
        Applies a new CDN/base URL to the given URL.

        If a URL is absolute, we skip switching the host since it could
        be a hostname that isn't behind our CDN, and we could unintentionally
        break the URL overall.
        """

        # The URL can't be empty.
        if not url:
            return url

        _, netloc, path, params, query, fragment = urlparse(url)

        # If this is an absolute URL, just return it as is.  It could be a domain
        # that isn't ours, and thus CDNing it would actually break it.
        if netloc:
            return url

        return urlunparse(('', base_url, path, params, query, fragment))

    @cached_property
    def _original_course(self):
        """
        Returns the course from the modulestore.
        """
        log.warning('Falling back on modulestore to get course information for %s', self.id)
        return modulestore().get_course(self.id)

    @property
    def allow_public_wiki_access(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.allow_public_wiki_access

    @property
    def textbooks(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.textbooks

    @property
    def pdf_textbooks(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.pdf_textbooks

    @property
    def html_textbooks(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.html_textbooks

    @property
    def hide_progress_tab(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.hide_progress_tab

    @property
    def edxnotes(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.edxnotes

    @property
    def enable_ccx(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.enable_ccx

    @property
    def course_visibility(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.course_visibility

    @property
    def teams_enabled(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.teams_enabled

    @property
    def show_calculator(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.show_calculator

    @property
    def edxnotes_visibility(self):
        """
        TODO: move this to the model.
        """
        return self._original_course.edxnotes_visibility

    def __str__(self):
        """Represent ourselves with the course key."""
        return six.text_type(self.id)
Beispiel #6
0
class CertificateInvalidation(TimeStampedModel):
    """
    Model for storing Certificate Invalidation.

    .. no_pii:
    """
    generated_certificate = models.ForeignKey(GeneratedCertificate,
                                              on_delete=models.CASCADE)
    invalidated_by = models.ForeignKey(User, on_delete=models.CASCADE)
    notes = models.TextField(default=None, null=True)
    active = models.BooleanField(default=True)

    # This is necessary because CMS does not install the certificates app, but
    # this code is run when other models in this file are imported there (or in
    # common code). Simple History will attempt to connect to the installed
    # model in the certificates app, which will fail.
    if 'certificates' in apps.app_configs:
        history = HistoricalRecords()

    class Meta:
        app_label = "certificates"

    def __str__(self):
        return "Certificate %s, invalidated by %s on %s." % \
               (self.generated_certificate, self.invalidated_by, self.created)

    def deactivate(self):
        """
        Deactivate certificate invalidation by setting active to False.
        """
        self.active = False
        self.save()

    @classmethod
    def get_certificate_invalidations(cls, course_key, student=None):
        """
        Return certificate invalidations filtered based on the provided course and student (if provided),

        Returned value is JSON serializable list of dicts, dict element would have the following key-value pairs.
         1. id: certificate invalidation id (primary key)
         2. user: username of the student to whom certificate belongs
         3. invalidated_by: user id of the instructor/support user who invalidated the certificate
         4. created: string containing date of invalidation in the following format "December 29, 2015"
         5. notes: string containing notes regarding certificate invalidation.
        """
        certificate_invalidations = cls.objects.filter(
            generated_certificate__course_id=course_key,
            active=True,
        )
        if student:
            certificate_invalidations = certificate_invalidations.filter(
                generated_certificate__user=student)
        data = []
        for certificate_invalidation in certificate_invalidations:
            data.append({
                'id':
                certificate_invalidation.id,
                'user':
                certificate_invalidation.generated_certificate.user.username,
                'invalidated_by':
                certificate_invalidation.invalidated_by.username,
                'created':
                certificate_invalidation.created.strftime("%B %d, %Y"),
                'notes':
                certificate_invalidation.notes,
            })
        return data

    @classmethod
    def has_certificate_invalidation(cls, student, course_key):
        """Check that whether the student in the course has been invalidated
        for receiving certificates.

        Arguments:
            student (user): logged-in user
            course_key (CourseKey): The course associated with the certificate.

        Returns:
             Boolean denoting whether the student in the course is invalidated
             to receive certificates
        """
        return cls.objects.filter(
            generated_certificate__course_id=course_key,
            active=True,
            generated_certificate__user=student).exists()
Beispiel #7
0
class Product(AbstractProduct):
    course = models.ForeignKey('courses.Course', null=True, blank=True, related_name='products')
    expires = models.DateTimeField(null=True, blank=True,
                                   help_text=_('Last date/time on which this product can be purchased.'))
    history = HistoricalRecords()
Beispiel #8
0
class Page(models.Model):
    """Customisable content that belongs to a challenge."""

    UP = "UP"
    DOWN = "DOWN"
    FIRST = "FIRST"
    LAST = "LAST"

    ALL = "ALL"
    REGISTERED_ONLY = "REG"
    ADMIN_ONLY = "ADM"

    PERMISSIONS_CHOICES = (
        (ALL, "All"),
        (REGISTERED_ONLY, "Participants only"),
        (ADMIN_ONLY, "Administrators only"),
    )

    display_title = models.CharField(
        max_length=255,
        blank=False,
    )
    slug = AutoSlugField(populate_from="display_title", max_length=64)
    challenge = models.ForeignKey(
        "challenges.Challenge",
        help_text="Which challenge does this page belong to?",
        on_delete=models.PROTECT,
    )
    permission_level = models.CharField(max_length=3,
                                        choices=PERMISSIONS_CHOICES,
                                        default=ALL)
    order = models.IntegerField(
        editable=False,
        default=1,
        help_text="Determines order in which page appear in site menu",
    )
    hidden = models.BooleanField(
        default=False, help_text="Do not display this page in site menu")
    html = models.TextField(blank=True, default="")
    history = HistoricalRecords()

    def __str__(self):
        if self.display_title:
            return self.display_title
        else:
            return self.slug

    def save(self, *args, **kwargs):
        # when saving for the first time only, put this page last in order
        if not self.id:
            # get max value of order for current pages.
            try:
                max_order = Page.objects.filter(
                    challenge=self.challenge).aggregate(Max("order"))
            except ObjectDoesNotExist:
                max_order = None
            try:
                self.order = max_order["order__max"] + 1
            except TypeError:
                self.order = 1

        self.html = clean(self.html)

        super().save(*args, **kwargs)

        self.assign_permissions()

    def assign_permissions(self):
        """Give the right groups permissions to this object."""
        admins_group = self.challenge.admins_group
        participants_group = self.challenge.participants_group

        if self.permission_level == self.ALL:
            assign_perm(f"view_{self._meta.model_name}", admins_group, self)
            assign_perm(f"view_{self._meta.model_name}", participants_group,
                        self)
        elif self.permission_level == self.REGISTERED_ONLY:
            assign_perm(f"view_{self._meta.model_name}", admins_group, self)
            assign_perm(f"view_{self._meta.model_name}", participants_group,
                        self)
        elif self.permission_level == self.ADMIN_ONLY:
            assign_perm(f"view_{self._meta.model_name}", admins_group, self)
            remove_perm(f"view_{self._meta.model_name}", participants_group,
                        self)
        else:
            raise ValueError(
                f"Unknown permissions level '{self.permission_level}'. "
                "I don't know which groups to give permissions to this object")

    def can_be_viewed_by(self, user):
        """Is user allowed to view this?"""
        if self.permission_level == self.ALL:
            return True
        else:
            return user.has_perm(f"view_{self._meta.model_name}", self)

    @property
    def detail_context(self):
        context = {}

        cleaned_html = clean(self.html)

        if "project_statistics" in cleaned_html:
            cleaned_html = self._substitute_geochart(html=cleaned_html)
            context["includes_geochart"] = True

        context["cleaned_html"] = cleaned_html

        return context

    def _substitute_geochart(self, *, html):
        users = self.challenge.get_participants().select_related(
            "user_profile", "verification")
        country_data = (users.exclude(
            user_profile__country="").values("user_profile__country").annotate(
                country_count=Count("user_profile__country")).order_by(
                    "-country_count").values_list("user_profile__country",
                                                  "country_count"))
        content = render_to_string(
            "grandchallenge/partials/geochart.html",
            {
                "user_count":
                users.count(),
                "country_data": [{
                    "id": countries.numeric(c[0], padded=True),
                    "participants": c[1],
                } for c in country_data],
            },
        )

        s = Substitution(
            tag_name="project_statistics",
            replacement=content,
        )
        return s.sub(html)

    def move(self, move):
        if move == self.UP:
            mm = Page.objects.get(challenge=self.challenge,
                                  order=self.order - 1)
            mm.order += 1
            mm.save()
            self.order -= 1
            self.save()
        elif move == self.DOWN:
            mm = Page.objects.get(challenge=self.challenge,
                                  order=self.order + 1)
            mm.order -= 1
            mm.save()
            self.order += 1
            self.save()
        elif move == self.FIRST:
            pages = Page.objects.filter(challenge=self.challenge)
            idx = index(pages, self)
            pages[idx].order = pages[0].order - 1
            pages = sorted(pages, key=lambda page: page.order)
            self.normalize_page_order(pages)
        elif move == self.LAST:
            pages = Page.objects.filter(challenge=self.challenge)
            idx = index(pages, self)
            pages[idx].order = pages[len(pages) - 1].order + 1
            pages = sorted(pages, key=lambda page: page.order)
            self.normalize_page_order(pages)

    @staticmethod
    def normalize_page_order(pages):
        """Make sure order in pages Queryset starts at 1 and increments 1 at
        every page. Saves all pages

        """
        for idx, page in enumerate(pages):
            page.order = idx + 1
            page.save()

    def get_absolute_url(self):
        url = reverse(
            "pages:detail",
            kwargs={
                "challenge_short_name": self.challenge.short_name,
                "slug": self.slug,
            },
        )
        return url

    class Meta:
        # make sure a single site never has two pages with the same name
        # because page names are used as keys in urls
        unique_together = (("challenge", "slug"), )
        # when getting a list of these objects this ordering is used
        ordering = ["challenge", "order"]
Beispiel #9
0
class User(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100, blank=True)
    email = models.EmailField(max_length=100, blank=True)
    history = HistoricalRecords()
Beispiel #10
0
class DiscussionsConfiguration(TimeStampedModel):
    """
    Associates a learning context with discussion provider and configuration
    """

    context_key = LearningContextKeyField(
        primary_key=True,
        db_index=True,
        unique=True,
        max_length=255,
        # Translators: A key specifying a course, library, program,
        # website, or some other collection of content where learning
        # happens.
        verbose_name=_("Learning Context Key"),
    )
    enabled = models.BooleanField(
        default=True,
        help_text=
        _("If disabled, the discussions in the associated learning context/course will be disabled."
          ))
    lti_configuration = models.ForeignKey(
        LtiConfiguration,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        help_text=_("The LTI configuration data for this context/provider."),
    )
    enable_in_context = models.BooleanField(
        default=True,
        help_text=_(
            "If enabled, discussion topics will be created for each non-graded unit in the course. "
            "A UI for discussions will show up with each unit."))
    enable_graded_units = models.BooleanField(
        default=False,
        help_text=
        _("If enabled, discussion topics will be created for graded units as well."
          ))
    unit_level_visibility = models.BooleanField(
        default=False,
        help_text=
        _("If enabled, discussions will need to be manually enabled for each unit."
          ))
    plugin_configuration = JSONField(
        blank=True,
        default={},
        help_text=_(
            "The plugin configuration data for this context/provider."),
    )
    provider_type = models.CharField(
        blank=False,
        max_length=100,
        verbose_name=_("Discussion provider"),
        help_text=_("The discussion tool/provider's id"),
        default=DEFAULT_PROVIDER_TYPE,
    )
    history = HistoricalRecords()

    def clean(self):
        """
        Validate the model.
        Currently, this only support courses, this can be extended
        whenever discussions are available in other contexts
        """
        if not CourseOverview.course_exists(self.context_key):
            raise ValidationError(
                'Context Key should be an existing learning context.')

    def __str__(self):
        return "DiscussionsConfiguration(context_key='{context_key}', provider='{provider}', enabled={enabled})".format(
            context_key=self.context_key,
            provider=self.provider_type,
            enabled=self.enabled,
        )

    def supports_in_context_discussions(self):
        """
        Returns is the provider supports in-context discussions
        """
        return AVAILABLE_PROVIDER_MAP.get(self.provider_type, {}).get(
            'supports_in_context_discussions', False)

    def supports(self, feature: str) -> bool:
        """
        Check if the provider supports some feature
        """
        features = AVAILABLE_PROVIDER_MAP.get(
            self.provider_type)['features'] or []
        has_support = bool(feature in features)
        return has_support

    def supports_lti(self) -> bool:
        """Returns a boolean indicating if the provider supports lti discussion view."""
        return AVAILABLE_PROVIDER_MAP.get(self.provider_type,
                                          {}).get('supports_lti', False)

    @classmethod
    def is_enabled(cls, context_key: CourseKey) -> bool:
        """
        Check if there is an active configuration for a given course key

        Default to False, if no configuration exists
        """
        configuration = cls.get(context_key)
        return configuration.enabled

    @classmethod
    def get(cls: Type[T], context_key: CourseKey) -> T:
        """
        Lookup a model by context_key
        """
        try:
            configuration = cls.objects.get(context_key=context_key)
        except cls.DoesNotExist:
            configuration = cls(
                context_key=context_key,
                enabled=DEFAULT_CONFIG_ENABLED,
                provider_type=DEFAULT_PROVIDER_TYPE,
            )
        return configuration

    @property
    def available_providers(self) -> List[str]:
        return ProviderFilter.current(
            course_key=self.context_key).available_providers

    @classmethod
    def get_available_providers(cls, context_key: CourseKey) -> List[str]:
        return ProviderFilter.current(
            course_key=context_key).available_providers

    @classmethod
    def lti_discussion_enabled(cls, course_key: CourseKey) -> bool:
        """
        Checks if LTI discussion is enabled for this course.

        Arguments:
            course_key: course locator.
        Returns:
            Boolean indicating weather or not this course has lti discussion enabled.
        """
        discussion_provider = cls.get(course_key)
        return (discussion_provider.enabled
                and discussion_provider.supports_lti()
                and discussion_provider.lti_configuration is not None)
class PettycashTerminal(models.Model):
    name = models.CharField(
        max_length=300,
        blank=True,
        null=True,
        help_text=
        "Name, initially as reported by the firmware. Potentially not unique!",
    )
    fingerprint = models.CharField(
        max_length=64,
        blank=True,
        null=True,
        help_text="SHA256 fingerprint of the client certificate.",
    )
    nonce = models.CharField(max_length=64,
                             blank=True,
                             null=True,
                             help_text="256 bit nonce (as HEX)")
    date = models.DateTimeField(
        blank=True,
        null=True,
        help_text="Time and date the device was last seen",
        auto_now_add=True,
    )
    accepted = models.BooleanField(
        default=False,
        help_text=
        "Wether an administrator has checked the fingerprint against the display on the device and accepted it.",
    )
    history = HistoricalRecords()

    def __str__(self):
        return "%s@%s %s...%s" % (
            self.name,
            self.date.strftime("%Y/%m/%d %H:%M"),
            self.fingerprint[:3],
            self.fingerprint[-3:],
        )

    def save(self, *args, **kwargs):
        cutoff = timezone.now() - timedelta(
            minutes=settings.PETTYCASH_TERMS_MINS_CUTOFF)

        # Drop anything that is too old; and only keep the most recent up to
        # a cap -- feeble attempt at foiling obvious DOS. Mainly as the tags
        # can be as short as 32 bits and we did not want to also add a shared
        # scecret in the Arduino code. As these easily get committed to github
        # by accident.
        stale = (PettycashTerminal.objects.all().filter(
            Q(accepted=False)).order_by("date"))
        if len(stale) > settings.PETTYCASH_TERMS_MAX_UNKNOWN:
            lst = (User.objects.all().filter(
                groups__name=settings.PETTYCASH_ADMIN_GROUP).values_list(
                    "email", flat=True))
            emailPlain(
                "pettycash-dos-warn.txt",
                toinform=lst,
                context={
                    "base": settings.BASE,
                    "settings": settings,
                    "stale": stale
                },
            )
            logger.info("DOS mail set about too many terminals in waiting.")
        todel = set()
        for s in stale.filter(Q(date__lt=cutoff)):
            todel.add(s)
        for s in stale[settings.PETTYCASH_TERMS_MIN_UNKNOWN:]:
            todel.add(s)
        for s in todel:
            s.delete()

        return super(PettycashTerminal, self).save(*args, **kwargs)
Beispiel #12
0
class Item(models.Model):
    """Model representing a specific item that can be assigned to a project."""
    item_num = models.AutoField(primary_key=True)
    item_uuid = models.UUIDField(default=uuid.uuid4)
    prj = models.ForeignKey('Project',
                            related_name='items',
                            on_delete=models.SET_NULL,
                            null=True,
                            blank=True)
    notes = models.CharField(max_length=500, blank=True)
    date_added = models.DateField(null=True, blank=True)
    history = HistoricalRecords()

    ITEM_STATUS = (
        ('Available', 'Available'),
        ('Reserved', 'Reserved'),
        ('Deployed', 'Deployed'),
    )
    status = models.TextField(
        max_length=25,
        choices=ITEM_STATUS,
        default='Available',
    )

    ITEM_DESCRIPTION = (
        ('Building Materials', 'Building Materials'),
        ('Living Room', 'Living Room'),
        ('Kitchen', 'Kitchen'),
        ('Bedroom', 'Bedroom'),
        ('Bathroom', 'Bathroom'),
        ('Clothing', 'Clothing'),
        ('Outdoor', 'Outdoor'),
        ('Pet Supplies', 'Pet Supplies'),
        ('Recreational', 'Recreational'),
        ('Vehicle', 'Vehicle'),
    )
    description = models.TextField(max_length=25,
                                   choices=ITEM_DESCRIPTION,
                                   default='Outdoor')

    ITEM_SECTOR = (
        ('North-West', 'NW'),
        ('North-Center', 'NC'),
        ('North-East', 'NE'),
        ('Central-West', 'CW'),
        ('Central-Center', 'CC'),
        ('Central-East', 'CE'),
        ('South-West', 'SW'),
        ('South-Center', 'SC'),
        ('South-East', 'SE'),
        ('Off Site', 'OS'),
    )
    sector = models.CharField(max_length=25, choices=ITEM_SECTOR, default='NW')

    def update_item_status(self):
        """Updates status assigned to project"""
        if self.prj:
            self.status = 'Reserved'

    def get_absolute_url(self):
        """Returns the project number built in to URL for access a particular project instance."""
        return reverse('item-detail', args=[str(self.item_num)])

    def __str__(self):
        """String for representing the item object."""
        if not self.prj:
            return 'Item Number: {0} -  Project Number: {1} - Status: {2} - Inventory Sector: {3} - Description: {4}' \
                .format(self.item_num,
                        "None",
                        self.status,
                        self.sector,
                        self.description)
        else:
            return 'Item Number: {0} -  Project Number: {1} - Status: {2} - Inventory Sector: {3} - Description: {4}' \
                .format(self.item_num,
                        self.prj.prj_num,
                        self.status,
                        self.sector,
                        self.description)
Beispiel #13
0
class Student(models.Model):
    name = models.CharField(max_length=50, db_index=True)
    FORM_STUDIES_CHOICES = (('БЮДЖЕТ', 'Бюджет'), ('ПВЗ', 'ПВЗ'))

    SEX_CHOICES = (('М', 'Мужской'), ('Ж', 'Женский'))

    BED_STATUS_CHOICES = (
        ('Студент', 'Студент'),
        ('Заочник', 'Заочник'),
        ('Семейник', 'Семейник'),
        ('Расселитель', 'Расселитель'),
        ('Староста этажа', 'Староста этажа'),
        ('Актив этажа', 'Актив этажа'),
        ('Санитарная комиссия', 'Санитарная комиссия'),
        ('СООПР', 'СООПР'),
        ('Женское', 'Женское'),
        ('Мужское', 'Мужское'),
        ('Занято', 'Занято'),
        ('Пусто', 'Пусто'),
    )

    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=50, db_index=True)
    bed_status = models.CharField(max_length=30,
                                  db_index=True,
                                  choices=BED_STATUS_CHOICES,
                                  blank=True,
                                  null=True)
    faculty = models.CharField(max_length=10,
                               db_index=True,
                               blank=True,
                               null=True)
    form_studies = models.CharField(max_length=10,
                                    db_index=True,
                                    choices=FORM_STUDIES_CHOICES,
                                    blank=True,
                                    null=True)
    group = models.CharField(max_length=10,
                             db_index=True,
                             blank=True,
                             null=True)
    sex = models.CharField(max_length=2,
                           db_index=True,
                           choices=SEX_CHOICES,
                           blank=True,
                           null=True)
    mobile_number = models.BigIntegerField(db_index=True,
                                           blank=True,
                                           null=True)
    fluorography = models.BooleanField(db_index=True, default=False)
    pediculosis = models.BooleanField(db_index=True, default=False)
    contract_number = models.CharField(max_length=15,
                                       db_index=True,
                                       blank=True,
                                       null=True)
    agreement_date = models.DateField(blank=True, null=True)
    registration = models.DateField(blank=True, null=True)
    citizenship = models.CharField(max_length=20,
                                   db_index=True,
                                   blank=True,
                                   null=True)
    date_of_birthday = models.DateField(blank=True, null=True)
    place_of_birthday = models.CharField(max_length=70,
                                         db_index=True,
                                         blank=True,
                                         null=True)
    document_number = models.CharField(max_length=20,
                                       db_index=True,
                                       blank=True,
                                       null=True)
    authority = models.CharField(max_length=100, blank=True, null=True)
    date_of_issue = models.DateField(blank=True, null=True)
    notation = models.TextField(db_index=True, blank=True, null=True)

    room = models.ForeignKey('Room',
                             related_name='students',
                             on_delete=models.CASCADE,
                             to_field='room_numb',
                             blank=True,
                             null=True)

    history = HistoricalRecords()

    def save_without_historical_record(self, *args, **kwargs):
        self.skip_history_when_saving = True
        try:
            ret = self.save(*args, **kwargs)
        finally:
            del self.skip_history_when_saving

        return ret

    def get_absolute_url(self):
        id = self.id
        return reverse('student_detail_url', kwargs={'id': id})

    def room_url(self):
        url = self.room
        return reverse('room_detail_url', kwargs={'room_det': url})

    def __str__(self):
        return self.name
class PettycashTransaction(models.Model):
    dst = models.ForeignKey(
        User,
        help_text="Whom to pay the money to",
        on_delete=models.CASCADE,
        related_name="isReceivedBy",
        blank=True,
        null=True,
    )
    src = models.ForeignKey(
        User,
        help_text="Whom paid you",
        on_delete=models.CASCADE,
        related_name="isSentBy",
        blank=True,
        null=True,
    )

    date = models.DateTimeField(blank=True,
                                null=True,
                                help_text="Date of transaction")

    amount = MoneyField(
        max_digits=8,
        decimal_places=2,
        null=True,
        default_currency="EUR",
        validators=[MinMoneyValidator(0)],
    )
    description = models.CharField(
        max_length=300,
        blank=True,
        null=True,
        help_text="Description / omschrijving van waarvoor deze betaling is",
    )

    history = HistoricalRecords()

    def url(self):
        return settings.BASE + self.path()

    def path(self):
        return reverse("transactions", kwargs={"pk": self.id})

    def __str__(self):
        if self.dst == self.src:
            return "@%s BALANCE %s" % (self.date, self.amount)
        return "@%s %s->%s '%s' %s" % (
            self.date,
            self.src,
            self.dst,
            self.description,
            self.amount,
        )

    def delete(self, *args, **kwargs):
        rc = super(PettycashTransaction, self).delete(*args, **kwargs)
        try:
            adjust_balance_cache(self, self.src, self.amount)
            adjust_balance_cache(self, self.dst, -self.amount)
        except Exception as e:
            logger.error("Transaction cache failure on delete: %s" % (e))

        return rc

    def refund_booking(self):
        """
        Refund a booking by doing a new 'reverse' booking, this way all amounts stay positive
        """
        new_transaction = PettycashTransaction()
        new_transaction.src = self.dst
        new_transaction.dst = self.src
        new_transaction.amount = self.amount
        new_transaction.description = "refund %s (%d)" % (self.description,
                                                          self.pk)
        new_transaction.save()

    def save(self, *args, **kwargs):
        bypass = False

        if kwargs is not None and "bypass" in kwargs:
            bypass = kwargs["bypass"]
            del kwargs["bypass"]
        if self.pk:
            if not bypass:
                raise ValidationError(
                    "you may not edit an existing Transaction - instead create a new one"
                )
            logger.info("Bypass used on save of %s" % self)

        if not self.date:
            self.date = timezone.now()

        if self.amount < Money(0, EUR):
            if not bypass:
                raise ValidationError("Blocked negative transaction.")
            logger.info("Bypass for negative transaction used on save of %s" %
                        self)

        rc = super(PettycashTransaction, self).save(*args, **kwargs)
        try:
            adjust_balance_cache(self, self.src, -self.amount)
            adjust_balance_cache(self, self.dst, self.amount)
        except Exception as e:
            logger.error("Transaction cache failure: %s" % (e))

        return rc
class Article(models.Model):
    """Default article model for Portfolio Manager for Companies.

    An article can be anything from an internship, hackathon, other announcements.
    """

    def __str__(self):
        return self.title

    # Each article must have atleast an author, a title and some content.
    author = models.ForeignKey(
        get_user_model(),
        on_delete=models.SET_NULL,
        null=True,
        blank=False,
        help_text="article author",
    )
    title = models.TextField(null=False, blank=False, help_text="the article title")
    content = models.TextField(null=False, blank=False, help_text="the article content")
    short_description = models.TextField(
        null=False,
        blank=False,
        help_text="short description of the company, appears in list of companies",
    )

    # The date the article was created
    created_date = models.DateTimeField(
        auto_now_add=True, help_text="Date the article was created"
    )

    # The date the article will be published (if it is approved by this date)
    # If not approved by this date, then it should be published as soon as it's approved
    publish_date = models.DateTimeField(help_text="Date the article was published")

    # The date the article will no longer be available (deadline for submitting applications)
    expiration_date = models.DateTimeField(
        null=True,
        blank=True,  # If ommited we assume it doesn't expire
        help_text="Valid until date",
    )

    is_approved = models.BooleanField(
        default=False, help_text="was this approved by an admin for publishing"
    )

    is_pinned = models.BooleanField(
        default=False, help_text="should be displayed in the pinned articles section"
    )

    university_public_note = models.TextField(
        null=True,
        blank=True,
        help_text="Informatii publice adaugate de universitate pentru acest articol",
    )
    university_private_note = models.TextField(
        null=True,
        blank=True,
        help_text="Informatii private adaugate de universitate pentru acest articol",
    )

    tags = models.ManyToManyField(ArticleTag)

    history = HistoricalRecords()

    def get_absolute_url(self):
        """Used to generate reverse URL for detail page by forms."""
        return reverse("articles:detail", kwargs={"pk": self.pk})
Beispiel #16
0
class Line(AbstractLine):
    history = HistoricalRecords()
class Printer(SafeDeleteModel):
    class Meta:
        default_manager_name = 'objects'

    PAUSE = 'PAUSE'
    NONE = 'NONE'
    ACTION_ON_FAILURE = (
        (NONE, 'Just notify me'),
        (PAUSE, 'Pause the printer and notify me'),
    )

    name = models.CharField(max_length=200, null=False)
    auth_token = models.CharField(max_length=28, unique=True, null=False, blank=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=False)
    current_print = models.OneToOneField('Print', on_delete=models.SET_NULL, null=True, blank=True, related_name='not_used')
    action_on_failure = models.CharField(
        max_length=10,
        choices=ACTION_ON_FAILURE,
        default=PAUSE,
    )
    watching_enabled = models.BooleanField(default=True, db_column="watching")
    tools_off_on_pause = models.BooleanField(default=True)
    bed_off_on_pause = models.BooleanField(default=False)
    retract_on_pause = models.FloatField(null=False, default=6.5)
    lift_z_on_pause = models.FloatField(null=False, default=2.5)
    detective_sensitivity = models.FloatField(null=False, default=1.0)

    archived_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    service_token = models.CharField(max_length=64, unique=True, db_index=True, null=True, blank=False)

    objects = PrinterManager()
    with_archived = SafeDeleteManager()

    if os.environ.get('ENALBE_HISTORY', '') == 'True':
        history = HistoricalRecords(excluded_fields=['updated_at'])

    @property
    def status(self):
        status_data = cache.printer_status_get(self.id)

        for k, v in status_data.items():
            status_data[k] = json.loads(v)

        return dict_or_none(status_data)

    @property
    def pic(self):
        pic_data = cache.printer_pic_get(self.id)

        return dict_or_none(pic_data)

    @property
    def settings(self):
        p_settings = cache.printer_settings_get(self.id)

        for key in ('webcam_flipV', 'webcam_flipH', 'webcam_rotate90'):
            p_settings[key] = p_settings.get(key, 'False') == 'True'
        p_settings['ratio169'] = p_settings.get('webcam_streamRatio', '4:3') == '16:9'

        if p_settings.get('temp_profiles'):
            p_settings['temp_profiles'] = json.loads(p_settings.get('temp_profiles'))

        return p_settings

    # should_watch and not_watching_reason follow slightly different rules
    # should_watch is used by the plugin. Therefore printing status is not a factor, otherwise we may have a feedback cycle:
    #    printer paused -> update server cache -> send should_watch to plugin -> udpate server
    # not_watching_reason is used by the web app and mobile app

    def should_watch(self):
        if not self.watching_enabled or self.user.dh_balance < 0:
            return False

        return self.current_print is not None and self.current_print.alert_muted_at is None

    def not_watching_reason(self):
        if not self.watching_enabled:
            return '"Watch for failures" is turned off'

        if self.user.dh_balance < 0:
            return "You have ran out of Detective Hours"

        if not self.actively_printing():
            return "Printer is not actively printing"

        if self.current_print is not None and self.current_print.alert_muted_at is not None:
            return "Alerts are muted for current print"

        return None

    def actively_printing(self):
        printer_cur_state = cache.printer_status_get(self.id, 'state')

        return printer_cur_state and json.loads(printer_cur_state).get('flags', {}).get('printing', False)

    def update_current_print(self, filename, current_print_ts):
        if current_print_ts == -1:      # Not printing
            if self.current_print:
                if self.current_print.started_at < (timezone.now() - timedelta(hours=10)):
                    self.unset_current_print()
                else:
                    LOGGER.warn(f'current_print_ts=-1 received when current print is still active. print_id: {self.current_print_id} - printer_id: {self.id}')

            return

        # currently printing

        if self.current_print:
            if self.current_print.ext_id == current_print_ts:
                return
            # Unknown bug in plugin that causes current_print_ts not unique

            if self.current_print.ext_id in range(current_print_ts - 20, current_print_ts + 20) and self.current_print.filename == filename:
                LOGGER.warn(
                    f'Apparently skewed print_ts received. ts1: {self.current_print.ext_id} - ts2: {current_print_ts} - print_id: {self.current_print_id} - printer_id: {self.id}')

                return
            LOGGER.warn(f'Print not properly ended before next start. Stale print_id: {self.current_print_id} - printer_id: {self.id}')
            self.unset_current_print()
            self.set_current_print(filename, current_print_ts)
        else:
            self.set_current_print(filename, current_print_ts)

    def unset_current_print(self):
        print = self.current_print
        self.current_print = None
        self.save()

        self.printerprediction.reset_for_new_print()

        if print.cancelled_at is None:
            print.finished_at = timezone.now()
            print.save()

        PrintEvent.create(print, PrintEvent.ENDED)
        self.send_should_watch_status()

    def set_current_print(self, filename, current_print_ts):
        if not current_print_ts or current_print_ts == -1:
            raise Exception(f'Invalid current_print_ts when trying to set current_print: {current_print_ts}')

        try:
            cur_print, _ = Print.objects.get_or_create(
                user=self.user,
                printer=self,
                ext_id=current_print_ts,
                defaults={'filename': filename.strip(), 'started_at': timezone.now()},
            )
        except IntegrityError:
            raise ResurrectionError('Current print is deleted! printer_id: {} | print_ts: {} | filename: {}'.format(self.id, current_print_ts, filename))

        if cur_print.ended_at():
            if cur_print.ended_at() > (timezone.now() - timedelta(seconds=30)):  # Race condition. Some msg with valid print_ts arrived after msg with print_ts=-1
                return
            else:
                raise ResurrectionError('Ended print is re-surrected! printer_id: {} | print_ts: {} | filename: {}'.format(self.id, current_print_ts, filename))

        self.current_print = cur_print
        self.save()

        self.printerprediction.reset_for_new_print()
        PrintEvent.create(cur_print, PrintEvent.STARTED)
        self.send_should_watch_status()

    ## return: succeeded? ##
    def resume_print(self, mute_alert=False):
        if self.current_print is None:  # when a link on an old email is clicked
            return False

        self.current_print.paused_at = None
        self.current_print.save()

        self.acknowledge_alert(Print.NOT_FAILED)
        self.send_octoprint_command('resume')

        return True

    ## return: succeeded? ##
    def pause_print(self):
        if self.current_print is None:
            return False

        self.current_print.paused_at = timezone.now()
        self.current_print.save()

        args = {'retract': self.retract_on_pause, 'lift_z': self.lift_z_on_pause}

        if self.tools_off_on_pause:
            args['tools_off'] = True

        if self.bed_off_on_pause:
            args['bed_off'] = True
        self.send_octoprint_command('pause', args=args)

        return True

    ## return: succeeded? ##
    def cancel_print(self):
        if self.current_print is None:  # when a link on an old email is clicked
            return False

        self.acknowledge_alert(Print.FAILED)
        self.send_octoprint_command('cancel')

        return True

    def set_alert(self):
        self.current_print.alerted_at = timezone.now()
        self.current_print.save()

    def acknowledge_alert(self, alert_overwrite):
        if not self.current_print.alerted_at:   # Not even alerted. Shouldn't be here. Maybe user error?
            return

        self.current_print.alert_acknowledged_at = timezone.now()
        self.current_print.alert_overwrite = alert_overwrite
        self.current_print.save()

    def mute_current_print(self, muted):
        self.current_print.alert_muted_at = timezone.now() if muted else None
        self.current_print.save()

        if muted:
            PrintEvent.create(self.current_print, PrintEvent.ALERT_MUTED)
        else:
            PrintEvent.create(self.current_print, PrintEvent.ALERT_UNMUTED)

        self.send_should_watch_status()

    # messages to printer

    def send_octoprint_command(self, command, args={}):
        channels.send_msg_to_printer(self.id, {'commands': [{'cmd': command, 'args': args}]})

    def send_should_watch_status(self):
        self.refresh_from_db()
        channels.send_msg_to_printer(self.id, {'remote_status': {'should_watch': self.should_watch()}})

    def __str__(self):
        return str(self.id)
Beispiel #18
0
class DiscussionsConfiguration(TimeStampedModel):
    """
    Associates a learning context with discussion provider and configuration
    """

    context_key = LearningContextKeyField(
        primary_key=True,
        db_index=True,
        unique=True,
        max_length=255,
        # Translators: A key specifying a course, library, program,
        # website, or some other collection of content where learning
        # happens.
        verbose_name=_("Learning Context Key"),
    )
    enabled = models.BooleanField(
        default=True,
        help_text=_("If disabled, the discussions in the associated learning context/course will be disabled.")
    )
    lti_configuration = models.ForeignKey(
        LtiConfiguration,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        help_text=_("The LTI configuration data for this context/provider."),
    )
    plugin_configuration = JSONField(
        blank=True,
        default={},
        help_text=_("The plugin configuration data for this context/provider."),
    )
    provider_type = models.CharField(
        blank=False,
        max_length=100,
        verbose_name=_("Discussion provider"),
        help_text=_("The discussion tool/provider's id"),
    )
    history = HistoricalRecords()

    def clean(self):
        """
        Validate the model.
        Currently, this only support courses, this can be extended
        whenever discussions are available in other contexts
        """
        if not CourseOverview.course_exists(self.context_key):
            raise ValidationError('Context Key should be an existing learning context.')

    def __str__(self):
        return "DiscussionsConfiguration(context_key='{context_key}', provider='{provider}', enabled={enabled})".format(
            context_key=self.context_key,
            provider=self.provider_type,
            enabled=self.enabled,
        )

    def supports(self, feature: str) -> bool:
        """
        Check if the provider supports some feature
        """
        features = AVAILABLE_PROVIDER_MAP.get(self.provider_type)['features'] or []
        has_support = bool(feature in features)
        return has_support

    @classmethod
    def is_enabled(cls, context_key: CourseKey) -> bool:
        """
        Check if there is an active configuration for a given course key

        Default to False, if no configuration exists
        """
        configuration = cls.get(context_key)
        return configuration.enabled

    # pylint: disable=undefined-variable
    @classmethod
    def get(cls, context_key: CourseKey) -> cls:
        """
        Lookup a model by context_key
        """
        try:
            configuration = cls.objects.get(context_key=context_key)
        except cls.DoesNotExist:
            configuration = cls(
                context_key=context_key,
                enabled=False,
                provider_type=DEFAULT_PROVIDER_TYPE,
            )
        return configuration

    # pylint: enable=undefined-variable

    @property
    def available_providers(self) -> list[str]:
        return ProviderFilter.current(course_key=self.context_key).available_providers

    @classmethod
    def get_available_providers(cls, context_key: CourseKey) -> list[str]:
        return ProviderFilter.current(course_key=context_key).available_providers
Beispiel #19
0
class Course(models.Model):
    site = models.ForeignKey('sites.Site',
                             verbose_name=_('Site'),
                             null=True,
                             blank=True,
                             on_delete=models.PROTECT)
    partner = models.ForeignKey('partner.Partner',
                                null=False,
                                blank=False,
                                on_delete=models.PROTECT)
    id = models.CharField(null=False,
                          max_length=255,
                          primary_key=True,
                          verbose_name='ID')
    name = models.CharField(null=False, max_length=255)
    verification_deadline = models.DateTimeField(
        null=True,
        blank=True,
        help_text=
        _('Last date/time on which verification for this product can be submitted.'
          ))
    created = models.DateTimeField(null=True, auto_now_add=True)
    modified = models.DateTimeField(null=True, auto_now=True)
    thumbnail_url = models.URLField(null=True, blank=True)
    history = HistoricalRecords()

    def __str__(self):
        return str(self.id)

    def _create_parent_seat(self):
        """ Create the parent seat product if it does not already exist. """
        parent, created = self.products.get_or_create(
            course=self,
            structure=Product.PARENT,
            product_class=ProductClass.objects.get(
                name=SEAT_PRODUCT_CLASS_NAME),
        )
        ProductCategory.objects.get_or_create(
            category=Category.objects.get(name='Seats'), product=parent)
        parent.title = 'Seat in {}'.format(self.name)
        parent.is_discountable = True
        parent.attr.course_key = self.id
        parent.save()

        if created:
            logger.debug('Created new parent seat [%d] for [%s].', parent.id,
                         self.id)
        else:
            logger.debug('Parent seat [%d] already exists for [%s].',
                         parent.id, self.id)

    @transaction.atomic
    def save(self,
             force_insert=False,
             force_update=False,
             using=None,
             update_fields=None):
        super(Course, self).save(force_insert, force_update, using,
                                 update_fields)
        self._create_parent_seat()

    def publish_to_lms(self):
        """ Publish Course and Products to LMS. """
        return LMSPublisher().publish(self)

    @classmethod
    def is_mode_verified(cls, mode):
        """ Returns True if the mode is verified, otherwise False. """
        return mode.lower() in ('verified', 'professional', 'credit')

    @classmethod
    def certificate_type_for_mode(cls, mode):
        mode = mode.lower()

        if mode == 'no-id-professional':
            return 'professional'
        if mode == 'audit':
            # Historically, users enrolled in an 'audit' mode have not received a certificate.
            return ''

        return mode

    @property
    def type(self):
        """ Returns the type of the course (based on the available seat types). """
        seat_types = [
            getattr(seat.attr, 'certificate_type', '').lower()
            for seat in self.seat_products
        ]
        if 'credit' in seat_types:
            return 'credit'
        if 'professional' in seat_types or 'no-id-professional' in seat_types:
            return 'professional'
        # This is checking for the Verified and Audit case, but Audit has no certificate type
        # so it is returned as the empty string.
        if 'verified' in seat_types and ('' in seat_types
                                         or 'honor' in seat_types):
            return 'verified'
        if 'verified' in seat_types:
            return 'verified-only'
        return 'audit'

    @property
    def parent_seat_product(self):
        """ Returns the course seat parent Product. """
        return self.products.get(product_class__name=SEAT_PRODUCT_CLASS_NAME,
                                 structure=Product.PARENT)

    @property
    def seat_products(self):
        """ Returns a queryset of course seat Products related to this course. """
        return self.parent_seat_product.children.all().prefetch_related(
            'stockrecords')

    @property
    def enrollment_code_product(self):
        """Returns this course's enrollment code if it exists and is active."""
        enrollment_code = self.get_enrollment_code()
        if enrollment_code:
            info = Selector().strategy().fetch_for_product(enrollment_code)
            if info.availability.is_available_to_buy:
                return enrollment_code
        return None

    def get_course_seat_name(self, certificate_type):
        """ Returns the name for a course seat. """
        name = u'Seat in {}'.format(self.name)

        if certificate_type != '':
            name += u' with {} certificate'.format(certificate_type)

        return name

    @transaction.atomic
    def create_or_update_seat(
        self,
        certificate_type,
        id_verification_required,
        price,
        credit_provider=None,
        expires=None,
        credit_hours=None,
        remove_stale_modes=True,
        create_enrollment_code=False,
        sku=None,
    ):
        """
        Creates and updates course seat products.
        IMPORTANT: Requires the Partner sku (from the stock record) to be passed in for updates.

        Arguments:
            certificate_type(str): The seat type.
            id_verification_required(bool): Whether an ID verification is required.
            price(int): Price of the seat.
            partner(Partner): Site partner.

        Optional arguments:
            credit_provider(str): Name of the organization that provides the credit
            expires(datetime): Date when the seat type expires.
            credit_hours(int): Number of credit hours provided.
            remove_stale_modes(bool): Remove stale modes.
            create_enrollment_code(bool): Whether an enrollment code is created in addition to the seat.
            sku(str): The partner_sku for the product stored as part of the Stock Record. This is used
                to perform a GET on the seat as a unique identifier both Ecommerce and Discovery know about.

        Returns:
            Product:  The seat that has been created or updated.
        """
        certificate_type = certificate_type.lower()
        course_id = str(self.id)

        try:
            product_id = StockRecord.objects.get(
                partner_sku=sku, partner=self.partner).product_id
            seat = self.seat_products.get(id=product_id)
            logger.info(
                'Retrieved course seat child product with certificate type [%s] for [%s] from database.',
                certificate_type, course_id)
        except (StockRecord.DoesNotExist, Product.DoesNotExist):
            seat = Product()
            logger.info(
                'Course seat product with certificate type [%s] for [%s] does not exist. Attempted look up using sku '
                '[%s]. Instantiated a new instance.', certificate_type,
                course_id, sku)

        seat.course = self
        seat.structure = Product.CHILD
        seat.parent = self.parent_seat_product
        seat.is_discountable = True
        seat.expires = expires

        id_verification_required_query = Q(
            attributes__name='id_verification_required',
            attribute_values__value_boolean=id_verification_required)
        seat.title = self.get_course_seat_name(certificate_type)

        seat.save()

        # If a ProductAttribute is saved with a value of None or the empty string, the ProductAttribute is deleted.
        # As a consequence, Seats derived from a migrated "audit" mode do not have a certificate_type attribute.
        seat.attr.certificate_type = certificate_type
        seat.attr.course_key = course_id
        seat.attr.id_verification_required = id_verification_required
        if certificate_type in ENROLLMENT_CODE_SEAT_TYPES and create_enrollment_code:
            self._create_or_update_enrollment_code(certificate_type,
                                                   id_verification_required,
                                                   self.partner, price,
                                                   expires)

        if credit_provider:
            seat.attr.credit_provider = credit_provider

        if credit_hours:
            seat.attr.credit_hours = credit_hours

        seat.attr.save()

        try:
            stock_record = StockRecord.objects.get(product=seat,
                                                   partner=self.partner)
            logger.info(
                'Retrieved course seat product stock record with certificate type [%s] for [%s] from database.',
                certificate_type, course_id)
        except StockRecord.DoesNotExist:
            partner_sku = generate_sku(seat, self.partner)
            stock_record = StockRecord(product=seat,
                                       partner=self.partner,
                                       partner_sku=partner_sku)
            logger.info(
                'Course seat product stock record with certificate type [%s] for [%s] does not exist. '
                'Instantiated a new instance.', certificate_type, course_id)

        stock_record.price_excl_tax = price
        stock_record.price_currency = settings.OSCAR_DEFAULT_CURRENCY
        stock_record.save()

        if remove_stale_modes and self.certificate_type_for_mode(
                certificate_type) == 'professional':
            id_verification_required_query = Q(
                attributes__name='id_verification_required',
                attribute_values__value_boolean=not id_verification_required)
            certificate_type_query = Q(
                attributes__name='certificate_type',
                attribute_values__value_text=certificate_type)

            # Delete seats with a different verification requirement, assuming the seats
            # have not been purchased.
            self.seat_products.filter(certificate_type_query).annotate(
                orders=Count('line')).filter(id_verification_required_query,
                                             orders=0).delete()

        return seat

    def get_enrollment_code(self):
        """ Returns an enrollment code Product related to this course. """
        try:
            # Current use cases dictate that only one enrollment code product exists for a given course
            return Product.objects.get(
                product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME,
                course=self)
        except Product.DoesNotExist:
            return None

    def _create_or_update_enrollment_code(self, seat_type,
                                          id_verification_required, partner,
                                          price, expires):
        """
        Creates an enrollment code product and corresponding stock record for the specified seat.
        Includes course ID and seat type as product attributes.

        Args:
            seat_type (str): Seat type.
            partner (Partner): Seat provider set in the stock record.
            price (Decimal): Price of the seat.
            expires (datetime): Date when the enrollment code expires.

        Returns:
            Enrollment code product.
        """
        enrollment_code_product_class = ProductClass.objects.get(
            name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
        enrollment_code = self.get_enrollment_code()

        if not enrollment_code:
            title = 'Enrollment code for {seat_type} seat in {course_name}'.format(
                seat_type=seat_type, course_name=self.name)
            enrollment_code = Product(
                title=title,
                product_class=enrollment_code_product_class,
                course=self,
                expires=expires)
        enrollment_code.attr.course_key = self.id
        enrollment_code.attr.seat_type = seat_type
        enrollment_code.attr.id_verification_required = id_verification_required
        enrollment_code.save()

        try:
            stock_record = StockRecord.objects.get(product=enrollment_code,
                                                   partner=partner)
        except StockRecord.DoesNotExist:
            enrollment_code_sku = generate_sku(enrollment_code, partner)
            stock_record = StockRecord(product=enrollment_code,
                                       partner=partner,
                                       partner_sku=enrollment_code_sku)

        stock_record.price_excl_tax = price
        stock_record.price_currency = settings.OSCAR_DEFAULT_CURRENCY
        stock_record.save()

        return enrollment_code

    def toggle_enrollment_code_status(self, is_active):
        """Activate or deactivate an enrollment code.

        An enrollment code's expiration date should not exceed the accompanying
        seat's expiration date. If the seat does not have an expiration date, the
        enrollment code's expiration date is set to an arbitrary number of days
        in the future (365).

        Args:
            is_active (bool): Whether the enrollment code should be activated.
        """
        enrollment_code = self.get_enrollment_code()
        if enrollment_code:
            if is_active:
                seat = self.seat_products.get(
                    attributes__name='certificate_type',
                    attribute_values__value_text=enrollment_code.attr.seat_type
                )
                enrollment_code.expires = seat.expires if seat.expires else now(
                ) + timedelta(days=365)
            else:
                enrollment_code.expires = now() - timedelta(days=365)
            enrollment_code.save()
Beispiel #20
0
class Request(models.Model):
    class Statuses(models.IntegerChoices):
        DENIED = 0, "Elutasítva"
        REQUESTED = 1, "Felkérés"
        ACCEPTED = 2, "Elvállalva"
        RECORDED = 3, "Leforgatva"
        UPLOADED = 4, "Beírva"
        EDITED = 5, "Megvágva"
        ARCHIVED = 6, "Archiválva"
        DONE = 7, "Lezárva"
        CANCELED = 9, "Szervezők által lemondva"
        FAILED = 10, "Meghiúsult"

    title = models.CharField(max_length=200)
    created = models.DateTimeField(auto_now_add=True)
    start_datetime = models.DateTimeField()
    end_datetime = models.DateTimeField()
    deadline = models.DateField(blank=True)
    type = models.CharField(max_length=50)
    place = models.CharField(max_length=150)
    status = models.PositiveSmallIntegerField(choices=Statuses.choices,
                                              default=Statuses.REQUESTED)
    responsible = models.ForeignKey(
        User,
        related_name="responsible_user",
        on_delete=models.SET(get_sentinel_user),
        blank=True,
        null=True,
    )
    requester = models.ForeignKey(User,
                                  related_name="requester_user",
                                  on_delete=models.SET(get_sentinel_user))
    requested_by = models.ForeignKey(
        User,
        related_name="requested_by_user",
        on_delete=models.SET(get_sentinel_user),
        blank=True,
        null=True,
    )
    additional_data = JSONField(
        encoder=DjangoJSONEncoder,
        validators=[validate_request_additional_data],
        default=dict,
        blank=True,
    )
    history = HistoricalRecords()

    @property
    def url(self):
        return f"{settings.BASE_URL}/my-requests/{self.id}"

    @property
    def admin_url(self):
        return f"{settings.BASE_URL}/admin/requests/{self.id}"

    def clean(self):
        if not (self.start_datetime <= self.end_datetime):
            raise ValidationError(
                {"start_datetime": ["Must be earlier than end_datetime."]})
        if self.deadline and not (self.end_datetime.date() < self.deadline):
            raise ValidationError(
                {"deadline": ["Must be later than end of the event."]})

    def save(self, *args, **kwargs):
        if not self.deadline:
            self.deadline = (self.end_datetime + timedelta(weeks=3)).date()
        self.full_clean(exclude=["additional_data"])
        super(Request, self).save(*args, **kwargs)

    def __str__(self):
        return f"{self.title} || {self.start_datetime.date()}"
Beispiel #21
0
class Category(models.Model):
    """
    Defines the data structure of a certain type of features.
    """
    name = models.CharField(max_length=100)
    description = models.TextField(null=True, blank=True)
    project = models.ForeignKey('projects.Project', related_name='categories')
    created_at = models.DateTimeField(auto_now_add=True)
    creator = models.ForeignKey(settings.AUTH_USER_MODEL)
    order = models.IntegerField(default=0)
    status = models.CharField(choices=STATUS,
                              default=STATUS.active,
                              max_length=20)
    display_field = models.ForeignKey('categories.Field',
                                      null=True,
                                      related_name='display_field_of')
    expiry_field = models.ForeignKey('categories.Field',
                                     null=True,
                                     related_name='expiry_field_of')
    default_status = models.CharField(choices=DEFAULT_STATUS,
                                      default=DEFAULT_STATUS.pending,
                                      max_length=20)
    colour = models.TextField(default='#0033ff')
    symbol = models.ImageField(upload_to='symbols', null=True, max_length=500)

    objects = CategoryManager()
    history = HistoricalRecords()

    class Meta:
        ordering = ['order']

    def reorder_fields(self, order):
        """
        Changes the order in which fields are displayed on client side.

        Parameters
        -------
        order : List
            IDs of fields, ordered according to new display order
        """
        fields_to_save = []
        for idx, field_id in enumerate(order):
            field = self.fields.get(pk=field_id)
            field.order = idx
            fields_to_save.append(field)

        for field in fields_to_save:
            field.save()

    def get_query(self, rule):
        """
        Returns the SQL where clause for the category. It combines the where
        clause parts of each field in the category.

        Returns
        -------
        str
            SQL where clause for the rule
        """
        queries = ['(category_id = %s)' % self.id]

        if 'min_date' in rule:
            queries.append(
                '("contributions_observation".created_at >= to_date(\'' +
                rule['min_date'] + '\', \'YYYY-MM-DD HH24:MI\'))')

        if 'max_date' in rule:
            queries.append(
                '("contributions_observation".created_at <= to_date(\'' +
                rule['max_date'] + '\', \'YYYY-MM-DD HH24:MI\'))')

        for key in rule:
            if key not in ['min_date', 'max_date']:
                try:
                    field = self.fields.get_subclass(key=key)
                    queries.append(field.get_filter(rule[key]))
                except Field.DoesNotExist:
                    pass

        return '(%s)' % ' AND '.join(queries)

    def delete(self):
        """
        Deletes the category by setting its status to deleted.

        Notes
        -----
        It also deletes all contributions of that category.
        """
        from geokey.contributions.models import Observation
        Observation.objects.filter(category=self).delete()

        groups = self.project.usergroups.all()
        for usergroup in groups:
            if usergroup.filters is not None:
                f = usergroup.filters.pop(str(self.id), None)
                if f is not None:
                    usergroup.save()

        self.status = STATUS.deleted
        self.save()
Beispiel #22
0
class Organization(models.Model):
    name = models.CharField(max_length=100)
    slug = models.CharField(max_length=100, unique=True, db_index=True)
    history = HistoricalRecords()
Beispiel #23
0
class Post(models.Model, ModelDiffMixin):
    TYPE_POST = "post"
    TYPE_INTRO = "intro"
    TYPE_LINK = "link"
    TYPE_QUESTION = "question"
    TYPE_PAIN = "pain"
    TYPE_IDEA = "idea"
    TYPE_PROJECT = "project"
    TYPE_REFERRAL = "referral"
    TYPE_BATTLE = "battle"
    TYPE_WEEKLY_DIGEST = "weekly_digest"
    TYPES = [
        (TYPE_POST, "Текст"),
        (TYPE_INTRO, "#intro"),
        (TYPE_LINK, "Ссылка"),
        (TYPE_QUESTION, "Вопрос"),
        (TYPE_PAIN, "Боль"),
        (TYPE_IDEA, "Идея"),
        (TYPE_PROJECT, "Проект"),
        (TYPE_REFERRAL, "Рефералка"),
        (TYPE_BATTLE, "Батл"),
        (TYPE_WEEKLY_DIGEST, "Журнал Клуба"),
    ]

    TYPE_TO_EMOJI = {
        TYPE_POST: "📝",
        TYPE_INTRO: "🙋‍♀️",
        TYPE_LINK: "🔗",
        TYPE_QUESTION: "❓",
        TYPE_PAIN: "😭",
        TYPE_IDEA: "💡",
        TYPE_PROJECT: "🏗",
        TYPE_REFERRAL: "🏢",
        TYPE_BATTLE: "🤜🤛"
    }

    TYPE_TO_PREFIX = {
        TYPE_POST: "",
        TYPE_INTRO: "",
        TYPE_LINK: "➜",
        TYPE_PAIN: "Боль:",
        TYPE_IDEA: "Идея:",
        TYPE_QUESTION: "Вопрос:",
        TYPE_PROJECT: "Проект:",
        TYPE_REFERRAL: "Рефералка:",
        TYPE_BATTLE: "Батл:"
    }

    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    slug = models.CharField(max_length=128, unique=True, db_index=True)

    author = models.ForeignKey(User, related_name="posts", db_index=True, on_delete=models.CASCADE)
    type = models.CharField(max_length=32, choices=TYPES, default=TYPE_POST, db_index=True)
    topic = models.ForeignKey(Topic, related_name="posts", null=True, db_index=True, on_delete=models.SET_NULL)
    label = JSONField(null=True)

    title = models.TextField(null=False)
    text = models.TextField(null=False)
    html = models.TextField(null=True)
    url = models.URLField(null=True)
    image = models.URLField(null=True)

    metadata = JSONField(null=True)

    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
    updated_at = models.DateTimeField(auto_now=True)
    last_activity_at = models.DateTimeField(auto_now_add=True, db_index=True)
    published_at = models.DateTimeField(null=True, db_index=True)

    comment_count = models.IntegerField(default=0)
    view_count = models.IntegerField(default=0)
    upvotes = models.IntegerField(default=0, db_index=True)

    is_visible = models.BooleanField(default=True)
    is_visible_on_main_page = models.BooleanField(default=True)
    is_commentable = models.BooleanField(default=True)
    is_approved_by_moderator = models.BooleanField(default=False)
    is_public = models.BooleanField(default=False)
    is_pinned_until = models.DateTimeField(null=True)
    is_shadow_banned = models.BooleanField(default=False)

    history = HistoricalRecords(
        user_model=User,
        table_name="posts_history",
        excluded_fields=[
            "html",
            "created_at",
            "updated_at",
            "last_activity_at",
            "comment_count",
            "view_count",
            "upvotes",
        ],
    )

    class Meta:
        db_table = "posts"
        ordering = ["-created_at"]

    def to_dict(self):
        return {
            "slug": self.slug,
            "type": self.type,
            "upvotes": self.upvotes,
        }

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = generate_unique_slug(Post, str(Post.objects.count()))

        if not self.published_at and self.is_visible:
            self.published_at = datetime.utcnow()

        self.updated_at = datetime.utcnow()
        return super().save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse("show_post", kwargs={"post_type": self.type, "post_slug": self.slug})

    def increment_view_count(self):
        return Post.objects.filter(id=self.id).update(view_count=F("view_count") + 1)

    def increment_vote_count(self):
        return Post.objects.filter(id=self.id).update(upvotes=F("upvotes") + 1)

    @property
    def emoji(self):
        return self.TYPE_TO_EMOJI.get(self.type) or ""

    @property
    def prefix(self):
        return self.TYPE_TO_PREFIX.get(self.type) or ""

    @property
    def is_pinned(self):
        return self.is_pinned_until and self.is_pinned_until > datetime.utcnow()

    @property
    def is_searchable(self):
        return self.is_visible and not self.is_shadow_banned

    @property
    def description(self):
        return truncatechars(strip_tags(self.html or ""), 400)

    @property
    def effective_published_at(self):
        return self.published_at or self.created_at

    @classmethod
    def check_duplicate(cls, user, title):
        latest_user_post = Post.objects.filter(author=user).order_by("-created_at").first()
        return latest_user_post and latest_user_post.title == title

    @classmethod
    def visible_objects(cls):
        return cls.objects.filter(is_visible=True).select_related("topic", "author")

    @classmethod
    def objects_for_user(cls, user):
        return cls.visible_objects()\
            .extra({
                "is_voted": "select 1 from post_votes "
                            "where post_votes.post_id = posts.id "
                            f"and post_votes.user_id = '{user.id}'",
                "unread_comments": f"select unread_comments from post_views "
                                   f"where post_views.post_id = posts.id "
                                   f"and post_views.user_id = '{user.id}'"
            })  # TODO: i've been trying to use .annotate() here for 2 hours and I have no idea why it's not working

    @classmethod
    def check_rate_limits(cls, user):
        if user.is_moderator:
            return True

        day_post_count = Post.visible_objects()\
            .filter(author=user, created_at__gte=datetime.utcnow() - timedelta(hours=24))\
            .count()

        return day_post_count < settings.RATE_LIMIT_POSTS_PER_DAY

    @classmethod
    def get_user_intro(cls, user):
        return cls.objects.filter(author=user, type=Post.TYPE_INTRO).first()

    @classmethod
    def upsert_user_intro(cls, user, text, is_visible=True):
        intro, is_created = cls.objects.update_or_create(
            author=user,
            type=cls.TYPE_INTRO,
            defaults=dict(
                slug=user.slug,
                title=f"#intro от @{user.slug}",
                text=text,
                is_visible=is_visible,
                is_public=False,
            ),
        )
        if not is_created:
            intro.html = None
            intro.save()

        return intro
Beispiel #24
0
class ProductAttributeValue(AbstractProductAttributeValue):
    history = HistoricalRecords()
Beispiel #25
0
class TaskExecutor(CoreBase):
    task = models.ForeignKey(Task,
                             related_name='task_executors',
                             on_delete=models.CASCADE,
                             null=True)
    executor = models.ForeignKey(CoreUser,
                                 related_name='task_executor_users',
                                 verbose_name="Виконавець",
                                 on_delete=models.PROTECT,
                                 null=True)
    executor_role = models.CharField(verbose_name="Роль",
                                     choices=EXECUTOR_ROLE,
                                     null=True,
                                     max_length=50)
    detail = models.TextField(
        verbose_name="Уточення завдання",
        max_length=500,
        null=True,
    )
    result = models.TextField(
        verbose_name="Деталі виконаного підзавдання",
        max_length=500,
        null=True,
    )
    result_file = models.FileField(upload_to=get_result_file_path,
                                   null=True,
                                   verbose_name='Результат (файл)')
    result_document = models.ForeignKey(
        BaseDocument,
        on_delete=models.PROTECT,
        related_name='task_executors',
        null=True,
        help_text=
        "Результатом виконання завдання іноді може бути документ, але не завжти."
        + " Зазвичай достатньо описати результат")
    end_date = models.DateField(verbose_name="Уточнення кінцевого терміну",
                                null=True,
                                default=None,
                                blank=True)
    status = models.CharField(verbose_name="Статус завдання",
                              choices=TASK_EXECUTOR_STATUS,
                              max_length=20,
                              default=PENDING)
    sign = models.TextField(null=True, verbose_name='Цифровий підпис')
    approve_method = models.CharField(max_length=20,
                                      verbose_name="Метод підтвердження",
                                      choices=APPROVE_METHODS,
                                      null=True)
    sign_info = JSONField(
        null=True,
        verbose_name='Детальна інформація про накладений цифровий підпис')
    history = HistoricalRecords()

    class Meta:
        verbose_name = 'Виконання завдання'
        unique_together = (('task', 'executor'), ('task', 'executor_role'))

    def __str__(self):
        return str(self.executor)

    def save(self, *args, **kwargs):
        self.author = self.task.author
        super(CoreBase, self).save(*args, **kwargs)
Beispiel #26
0
class Proposal(TimeAuditModel):
    """ The proposals master """

    conference = models.ForeignKey(Conference, on_delete=models.CASCADE)
    proposal_section = models.ForeignKey(ProposalSection,
                                         verbose_name="Proposal Section",
                                         on_delete=models.CASCADE)
    proposal_type = models.ForeignKey(ProposalType,
                                      verbose_name="Proposal Type",
                                      on_delete=models.CASCADE)
    author = models.ForeignKey(User,
                               verbose_name="Primary Speaker",
                               on_delete=models.CASCADE)
    title = models.CharField(max_length=255)
    slug = AutoSlugField(max_length=255, populate_from=("title", ))
    description = models.TextField(default="")
    target_audience = models.PositiveSmallIntegerField(
        choices=ProposalTargetAudience.CHOICES,
        default=ProposalTargetAudience.BEGINNER,
        verbose_name="Target Audience",
    )
    video_url = models.URLField(
        blank=True,
        default="",
        help_text="Short 1-2 min video describing your talk",
    )
    prerequisites = models.TextField(blank=True, default="")
    content_urls = models.TextField(blank=True, default="")
    speaker_info = models.TextField(blank=True, default="")
    speaker_links = models.TextField(blank=True, default="")
    is_first_time_speaker = models.BooleanField(blank=True, default=False)
    status = models.PositiveSmallIntegerField(choices=ProposalStatus.CHOICES,
                                              default=ProposalStatus.DRAFT)
    review_status = models.PositiveSmallIntegerField(
        choices=ProposalReviewStatus.CHOICES,
        default=ProposalReviewStatus.YET_TO_BE_REVIEWED,
        verbose_name="Review Status",
    )
    deleted = models.BooleanField(default=False, verbose_name="Is Deleted?")
    history = HistoricalRecords()

    def __str__(self):
        return "{}, {}".format(self.title, self.proposal_type)

    def is_public(self):
        # TODO: Fix with proper enum
        return self.status == 2

    def get_slug(self):
        return slugify(self.title)

    def get_hashid(self):
        hashids = Hashids(min_length=5)
        return hashids.encode(self.id)

    def get_absolute_url(self):
        return reverse(
            "proposal-detail",
            args=[self.conference.slug,
                  self.get_slug(),
                  self.get_hashid()],
        )

    def get_update_url(self):
        return reverse("proposal-update",
                       args=[self.conference.slug, self.slug])

    def get_review_url(self):
        return reverse("proposal-review",
                       args=[self.conference.slug, self.slug])

    def get_vote_url(self):
        return reverse("proposal-reviewer-vote",
                       args=[self.conference.slug, self.slug])

    def get_secondary_vote_url(self):
        return reverse("proposal-reviewer-secondary-vote",
                       args=[self.conference.slug, self.slug])

    def get_delete_url(self):
        return reverse("proposal-delete",
                       args=[self.conference.slug, self.slug])

    def get_up_vote_url(self):
        return reverse("proposal-vote-up",
                       args=[self.conference.slug, self.slug])

    def get_down_vote_url(self):
        return reverse("proposal-vote-down",
                       args=[self.conference.slug, self.slug])

    def get_remove_vote_url(self):
        return reverse("proposal-vote-remove",
                       args=[self.conference.slug, self.slug])

    def get_comments_count(self):
        """ Show only public comments count """
        return ProposalComment.objects.filter(proposal=self,
                                              deleted=False,
                                              private=False,
                                              vote=False,
                                              reviewer=False).count()

    def get_reviews_comments_count(self):
        """ Show only private comments count """
        return ProposalComment.objects.filter(proposal=self,
                                              deleted=False,
                                              private=True,
                                              vote=False).count()

    def get_reviewer_comments_count(self, reviewer):
        """ Number of private comments by a reviewer """
        return ProposalComment.objects.filter(proposal=self,
                                              deleted=False,
                                              private=True,
                                              commenter=reviewer,
                                              vote=False).count()

    def get_votes_count(self):
        """ Show only the public comment count """
        votes = (ProposalVote.objects.filter(
            proposal=self).values("up_vote").annotate(
                counts=models.Count("up_vote")))
        votes = {item["up_vote"]: item["counts"] for item in votes}
        up_vote_count = votes.get(True, 0)
        down_vote_count = votes.get(False, 0)
        return up_vote_count - down_vote_count

    def get_reviewer_votes_count(self):
        """ Show sum of reviewer vote value. """
        return ProposalSectionReviewerVote.objects.filter(
            proposal=self).count()

    def get_reviewer_votes_count_by_value(self, vote_value):
        """ Show sum of reviewer votes for given vote value. """
        return ProposalSectionReviewerVote.objects.filter(
            proposal=self, vote_value__vote_value=vote_value).count()

    def get_reviewer_votes_sum(self):
        votes = ProposalSectionReviewerVote.objects.filter(proposal=self, )
        sum_of_votes = sum((v.vote_value.vote_value for v in votes))
        return sum_of_votes

    def get_reviewer_vote_value(self, reviewer):
        try:
            vote = ProposalSectionReviewerVote.objects.get(
                proposal=self,
                voter__conference_reviewer__reviewer=reviewer,
            )
            return vote.vote_value.vote_value
        except ProposalSectionReviewerVote.DoesNotExist:
            return 0

    def get_reviewers_count(self):
        """ Count of reviewers for given proposal section """
        return ProposalSectionReviewer.objects.filter(
            proposal_section=self.proposal_section).count()

    def has_negative_votes(self):
        """ Show sum of reviewer votes for given vote value. """
        return (ProposalSectionReviewerVote.objects.filter(
            proposal=self,
            vote_value__vote_value=ProposalReviewVote.NOT_ALLOWED,
        ).count() > 0)

    def to_response(self, request):
        """method will return dict which can be passed to response
        """
        author = "{} {}".format(self.author.first_name, self.author.last_name)
        data = {
            "id":
            self.id,
            "author":
            author,
            "title":
            self.title,
            "description":
            self.description,
            "target_audience":
            dict(ProposalTargetAudience.CHOICES)[self.target_audience],
            "status":
            dict(ProposalStatus.CHOICES)[self.status],
            "review_status":
            dict(ProposalReviewStatus.CHOICES)[self.review_status],
            "proposal_type":
            self.proposal_type.name,
            "proposal_section":
            self.proposal_section.name,
            "votes_count":
            self.get_votes_count(),
            "speaker_info":
            self.speaker_info,
            "speaker_links":
            self.speaker_links,
            "content_urls":
            self.content_urls,
            "conference":
            rf_reverse("conference-detail",
                       kwargs={"pk": self.conference_id},
                       request=request),
        }
        return data

    class Meta:
        unique_together = ("conference", "slug")
Beispiel #27
0
class Task(CoreBase):
    goal = models.CharField(verbose_name="Тип завдання",
                            choices=TASK_GOAL,
                            default=EXECUTE,
                            max_length=20)
    flow = models.ForeignKey(Flow,
                             related_name='tasks',
                             on_delete=models.CASCADE,
                             null=True,
                             blank=True)
    task_status = models.CharField(verbose_name="Статус завдання",
                                   choices=TASK_STATUS,
                                   max_length=100,
                                   default=PENDING)
    document = models.ForeignKey(BaseDocument,
                                 verbose_name="До документа",
                                 on_delete=models.CASCADE,
                                 null=True,
                                 blank=True)
    parent_task = models.ForeignKey('self',
                                    related_name='p_task',
                                    verbose_name="Резолюція залежна від",
                                    on_delete=models.SET_NULL,
                                    null=True,
                                    blank=True)
    order = models.PositiveSmallIntegerField(
        default=0, verbose_name="Порядок виконання завдань")
    # To save hierarchy in UI
    parent_node = models.ForeignKey('self',
                                    related_name='p_node',
                                    verbose_name="Резолюція до",
                                    on_delete=models.SET_NULL,
                                    null=True,
                                    blank=True)
    is_completed = models.BooleanField(verbose_name="Чи виконано",
                                       default=False)
    title = models.CharField(verbose_name="Завдання", max_length=200)
    controller = models.ForeignKey(CoreUser,
                                   related_name='%(class)s_controller',
                                   blank=True,
                                   verbose_name="Контролер",
                                   on_delete=models.PROTECT,
                                   null=True)
    author_is_controller = models.BooleanField(default=False,
                                               verbose_name="Я контролер")
    is_controlled = models.BooleanField(verbose_name="Чи перевірено",
                                        default=False)
    task_type = models.CharField(verbose_name="Тип завдання",
                                 choices=TASK_TYPE,
                                 default=TASK,
                                 max_length=20)
    start_date = models.DateField(verbose_name="Термін з",
                                  null=True,
                                  auto_now_add=True)
    end_date = models.DateField(verbose_name="Термін до",
                                null=True,
                                default=None,
                                blank=True)
    execute_date = models.DateField(verbose_name="Дата фактичного виконання",
                                    null=True)
    approve_type = models.CharField(verbose_name="Тип підтвердження",
                                    choices=APPROVE_TYPE,
                                    default=APPROVE,
                                    max_length=10)
    controller_comment = models.TextField(null=True,
                                          verbose_name="Коментар контролера")
    history = HistoricalRecords()

    class Meta:
        verbose_name = 'Завдання'
        verbose_name_plural = 'Завдання'
        ordering = ['date_add']
        permissions = [
            (CustomTaskPermissions.SET_CONTROLLER, "Встановлювати контролера"),
        ]

    def __str__(self):
        return self.title
Beispiel #28
0
class Line(AbstractLine):
    history = HistoricalRecords()
    effective_contract_discount_percentage = models.DecimalField(max_digits=8, decimal_places=5, null=True)
    effective_contract_discounted_price = models.DecimalField(max_digits=12, decimal_places=2, null=True)
Beispiel #29
0
class EducationSystem (models.Model):
    name = models.CharField(max_length=255, unique=True)
    history = HistoricalRecords()

    def __str__(self):
        return str(self.name)
Beispiel #30
0
class BaseDocument(IncomingDocument, OutgoingDocument, innerDocument, CoreBase):
    # General
    title = models.CharField(verbose_name="Назва", max_length=100, default='-')
    main_file = models.FileField(verbose_name="Головинй файл", upload_to=get_upload_document_path, null=True,
                                 max_length=500)
    document_cast = models.CharField(verbose_name="Вид документа", choices=DOCUMENT_CAST,
                                     max_length=100)
    reg_number = models.CharField(verbose_name="Реєстраціний номер", max_length=100, null=True)
    reg_date = models.DateField(verbose_name="Дата реєстрації", null=True, editable=False)

    comment = models.TextField(verbose_name="Короткий зміст", max_length=500, null=True, blank=True)
    document_linked_to = models.ManyToManyField('self', related_name='document_linked_to_document',
                                                verbose_name="До документа", blank=True)
    department = models.ForeignKey(Department, verbose_name="Департамент", on_delete=models.SET_NULL, null=True,
                                   blank=True)
    approvers_list = models.ManyToManyField(get_user_model(), verbose_name="На розгляд",
                                            help_text="Список користувачів які мають розглянути документ", blank=True)
    registration_type = models.CharField(verbose_name="&Тип реєстрації", choices=REGISTRATION_TYPES, default=MANUAL_REG,
                                         max_length=50)
    registration = models.ForeignKey(register_model.RegistrationJournal,
                                     verbose_name="Журнал реєстрації",
                                     on_delete=models.PROTECT, null=True, blank=True)
    case_index = models.CharField(verbose_name="Індекс  та  заголовок справи",
                                  max_length=100, null=True)

    case_number = models.CharField(verbose_name="Номер тому справи",
                                   max_length=100, null=True)

    preview = models.FileField(upload_to=get_preview_directory, null=True, editable=False, max_length=500)
    preview_pdf = models.FileField(upload_to=get_preview_directory, null=True, editable=False, max_length=500)
    status = models.CharField(verbose_name="Статус", max_length=100, default=PROJECT)
    history = HistoricalRecords()

    class Meta:
        verbose_name = 'Документ'
        verbose_name_plural = 'Документы'
        permissions = [
            (CustomDocumentPermissions.REGISTER_DOCUMENT, "Може реєструвати документ"),
            (CustomDocumentPermissions.CREATE_RESOLUTION, "Може створювати резолюцію"),
            (CustomDocumentPermissions.EXECUTE_RESOLUTION, "Може запускати резолюцію на виконання"),
            (CustomDocumentPermissions.SEND_TO_ARCHIVE, "Може відправляти документ в архів"),
            (CustomDocumentPermissions.SEND_TO_OVERVIEW, "Може відправляти документ на узгодження"),
            (CustomDocumentPermissions.VIEW_ON_REGISTRATION, "Переглядати документи на реєстрації"),
            (CustomDocumentPermissions.VIEW_REGISTERED, "Переглядати документи зареєстровані"),
            (CustomDocumentPermissions.VIEW_ON_RESOLUTION, "Переглядати документи на резолюції"),
            (CustomDocumentPermissions.VIEW_ON_EXECUTION, "Переглядати документи на виконанні"),
            (CustomDocumentPermissions.VIEW_COMPLETED, "Переглядати документи виконані"),
            (CustomDocumentPermissions.VIEW_ON_CONTROL, "Переглядати документи на контролі"),
            (CustomDocumentPermissions.VIEW_ARCHIVED, "Переглядати документи в архіві"),
            (CustomDocumentPermissions.VIEW_ON_AGREEMENT, "Переглядати документи на погодженні"),
            (CustomDocumentPermissions.VIEW_PROJECT, "Переглядати документи (проекти)"),
            (CustomDocumentPermissions.VIEW_REJECT, "Переглядати відмови"),
            (CustomDocumentPermissions.VIEW_CONCERTED, "Переглядати погоджені"),
            (CustomDocumentPermissions.VIEW_ON_SIGNING, "Переглядати на підписанні"),
            (CustomDocumentPermissions.VIEW_SIGNED, "Переглядати підписані"),
            (CustomDocumentPermissions.VIEW_TRANSFERRED, "Переглядати передані"),
            (CustomDocumentPermissions.CHANGE_DOCUMENT_DICTIONARY, "Редагувати довідники документів"),

        ]

    def __str__(self):
        return f'{self.get_document_cast_display()} документ "{self.reg_number}"'

    def save(self, *args, **kwargs):
        super(BaseDocument, self).save(*args, **kwargs)
Beispiel #31
0
class GeneratedCertificate(models.Model):
    """
    Base model for generated certificates

    .. pii: PII can exist in the generated certificate linked to in this model. Certificate data is currently retained.
    .. pii_types: name, username
    .. pii_retirement: retained
    """
    # Import here instead of top of file since this module gets imported before
    # the course_modes app is loaded, resulting in a Django deprecation warning.
    from common.djangoapps.course_modes.models import CourseMode  # pylint: disable=reimported

    # Only returns eligible certificates. This should be used in
    # preference to the default `objects` manager in most cases.
    eligible_certificates = EligibleCertificateManager()

    # Only returns eligible certificates for courses that have an
    # associated CourseOverview
    eligible_available_certificates = EligibleAvailableCertificateManager()

    # Normal object manager, which should only be used when ineligible
    # certificates (i.e. new audit certs) should be included in the
    # results. Django requires us to explicitly declare this.
    objects = models.Manager()

    MODES = Choices('verified', 'honor', 'audit', 'professional',
                    'no-id-professional', 'masters', 'executive-education')

    VERIFIED_CERTS_MODES = [CourseMode.VERIFIED, CourseMode.CREDIT_MODE, CourseMode.MASTERS, CourseMode.EXECUTIVE_EDUCATION]  # pylint: disable=line-too-long

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    course_id = CourseKeyField(max_length=255, blank=True, default=None)
    verify_uuid = models.CharField(max_length=32,
                                   blank=True,
                                   default='',
                                   db_index=True)
    download_uuid = models.CharField(max_length=32, blank=True, default='')
    download_url = models.CharField(max_length=128, blank=True, default='')
    grade = models.CharField(max_length=5, blank=True, default='')
    key = models.CharField(max_length=32, blank=True, default='')
    distinction = models.BooleanField(default=False)
    status = models.CharField(max_length=32, default='unavailable')
    mode = models.CharField(max_length=32, choices=MODES, default=MODES.honor)
    name = models.CharField(blank=True, max_length=255)
    created_date = models.DateTimeField(auto_now_add=True)
    modified_date = models.DateTimeField(auto_now=True)
    error_reason = models.CharField(max_length=512, blank=True, default='')

    # This is necessary because CMS does not install the certificates app, but it
    # imports this model's code. Simple History will attempt to connect to the installed
    # model in the certificates app, which will fail.
    if 'certificates' in apps.app_configs:
        history = HistoricalRecords()

    class Meta:
        unique_together = (('user', 'course_id'), )
        app_label = "certificates"

    @classmethod
    def certificate_for_student(cls, student, course_id):
        """
        This returns the certificate for a student for a particular course
        or None if no such certificate exits.
        """
        try:
            return cls.objects.get(user=student, course_id=course_id)
        except cls.DoesNotExist:
            pass

        return None

    @classmethod
    def course_ids_with_certs_for_user(cls, user):
        """
        Return a set of CourseKeys for which the user has certificates.

        Sometimes we just want to check if a user has already been issued a
        certificate for a given course (e.g. to test refund eligibility).
        Instead of checking if `certificate_for_student` returns `None` on each
        course_id individually, we instead just return a set of all CourseKeys
        for which this student has certificates all at once.
        """
        return {
            cert.course_id
            for cert in cls.objects.filter(user=user).only('course_id')
        }

    @classmethod
    def get_unique_statuses(cls, course_key=None, flat=False):
        """
        1 - Return unique statuses as a list of dictionaries containing the following key value pairs
            [
            {'status': 'status value from db', 'count': 'occurrence count of the status'},
            {...},
            ..., ]

        2 - if flat is 'True' then return unique statuses as a list
        3 - if course_key is given then return unique statuses associated with the given course

        :param course_key: Course Key identifier
        :param flat: boolean showing whether to return statuses as a list of values or a list of dictionaries.
        """
        query = cls.objects

        if course_key:
            query = query.filter(course_id=course_key)

        if flat:
            return query.values_list('status', flat=True).distinct()
        else:
            return query.values('status').annotate(count=Count('status'))

    def __repr__(self):
        return "<GeneratedCertificate: {course_id}, user={user}>".format(
            course_id=self.course_id, user=self.user)

    def invalidate(self):
        """
        Invalidate Generated Certificate by  marking it 'unavailable'.

        Following is the list of fields with their defaults
            1 - verify_uuid = '',
            2 - download_uuid = '',
            3 - download_url = '',
            4 - grade = ''
            5 - status = 'unavailable'
        """
        log.info(
            'Marking certificate as unavailable for {user} : {course}'.format(
                user=self.user.id, course=self.course_id))

        self.verify_uuid = ''
        self.download_uuid = ''
        self.download_url = ''
        self.grade = ''
        self.status = CertificateStatuses.unavailable
        self.save()
        COURSE_CERT_REVOKED.send_robust(
            sender=self.__class__,
            user=self.user,
            course_key=self.course_id,
            mode=self.mode,
            status=self.status,
        )

    def mark_notpassing(self, grade):
        """
        Invalidates a Generated Certificate by marking it as not passing
        """
        log.info(
            'Marking certificate as notpassing for {user} : {course}'.format(
                user=self.user.id, course=self.course_id))

        self.verify_uuid = ''
        self.download_uuid = ''
        self.download_url = ''
        self.grade = grade
        self.status = CertificateStatuses.notpassing
        self.save()
        COURSE_CERT_REVOKED.send_robust(
            sender=self.__class__,
            user=self.user,
            course_key=self.course_id,
            mode=self.mode,
            status=self.status,
        )

    def is_valid(self):
        """
        Return True if certificate is valid else return False.
        """
        return self.status == CertificateStatuses.downloadable

    def save(self, *args, **kwargs):  # pylint: disable=signature-differs
        """
        After the base save() method finishes, fire the COURSE_CERT_AWARDED
        signal iff we are saving a record of a learner passing the course.
        As well as the COURSE_CERT_CHANGED for any save event.
        """
        super().save(*args, **kwargs)
        COURSE_CERT_CHANGED.send_robust(
            sender=self.__class__,
            user=self.user,
            course_key=self.course_id,
            mode=self.mode,
            status=self.status,
        )
        if CertificateStatuses.is_passing_status(self.status):
            COURSE_CERT_AWARDED.send_robust(
                sender=self.__class__,
                user=self.user,
                course_key=self.course_id,
                mode=self.mode,
                status=self.status,
            )
Beispiel #32
0
class SocialDemographicData(models.Model):
    patient = models.ForeignKey(Patient)
    natural_of = models.CharField(max_length=50, null=True, blank=True)
    citizenship = models.CharField(max_length=50, null=True, blank=True)
    religion = models.ForeignKey(Religion, null=True, blank=True)
    profession = models.CharField(null=True, blank=True, max_length=50)
    occupation = models.CharField(null=True, blank=True, max_length=50)
    benefit_government = models.NullBooleanField(blank=True)
    payment = models.ForeignKey(Payment, null=True, blank=True)
    flesh_tone = models.ForeignKey(FleshTone, null=True, blank=True)
    schooling = models.ForeignKey(Schooling, null=True, blank=True)
    tv = models.IntegerField(null=True, blank=True, )
    dvd = models.IntegerField(null=True, blank=True, )
    radio = models.IntegerField(null=True, blank=True, )
    bath = models.IntegerField(null=True, blank=True, )
    automobile = models.IntegerField(null=True, blank=True, )
    wash_machine = models.IntegerField(null=True, blank=True, )
    refrigerator = models.IntegerField(null=True, blank=True, )
    freezer = models.IntegerField(null=True, blank=True, )
    house_maid = models.IntegerField(null=True, blank=True, )
    social_class = models.CharField(null=True, blank=True, max_length=10)

    # Changes to audit trail
    history = HistoricalRecords()
    changed_by = models.ForeignKey('auth.User')

    def __str__(self):
        return \
            str(self.patient)

    @property
    def _history_user(self):
        return self.changed_by

    @_history_user.setter
    def _history_user(self, value):
        self.changed_by = value

    @staticmethod
    def calculate_social_class(**keywords):
        #  According to IBGE:
        punctuation_table = {
            'tv':           {0: 0, 1: 1, 2: 2, 3: 3, 4: 4},
            'radio':        {0: 0, 1: 1, 2: 2, 3: 3, 4: 4},

            'dvd':          {0: 0, 1: 2, 2: 2, 3: 2, 4: 2},
            'wash_mashine': {0: 0, 1: 2, 2: 2, 3: 2, 4: 2},
            'freezer':      {0: 0, 1: 2, 2: 2, 3: 2, 4: 2},

            'bath':         {0: 0, 1: 4, 2: 5, 3: 6, 4: 7},
            'car':          {0: 0, 1: 4, 2: 7, 3: 9, 4: 9},
            'housemaid':    {0: 0, 1: 3, 2: 4, 3: 4, 4: 4},
            'refrigerator': {0: 0, 1: 4, 2: 4, 3: 4, 4: 4},

            'schooling':    {'1': 0, '2': 1, '3': 2, '4': 4, '5': 8}
        }
        points = 0

        for key_word in list(keywords.keys()):
            points += (punctuation_table[key_word])[keywords[key_word]]

        if 0 <= points <= 7:
            return 'E'
        elif 8 <= points <= 13:
            return 'D'
        elif 14 <= points <= 17:
            return 'C2'
        elif 18 <= points <= 22:
            return 'C1'
        elif 23 <= points <= 28:
            return 'B2'
        elif 29 <= points <= 34:
            return 'B1'
        elif 35 <= points <= 41:
            return 'A2'
        elif 42 <= points <= 46:
            return 'A1'
        else:
            return ''
class Question(models.Model):
    text = models.TextField()
    history = HistoricalRecords(bases=[HistoricalQuestion])

    def __str__(self):
        return self.text